feat: 同步报销流程与工作台改动
This commit is contained in:
@@ -87,7 +87,7 @@ test('platform admin users do not enter the personal workbench', () => {
|
||||
assert.equal(canAccessAppView(adminUser, 'workbench'), false)
|
||||
assert.equal(canAccessAppView(employeeUser, 'workbench'), true)
|
||||
assert.equal(getAccessibleViewIds(adminUser).includes('workbench'), false)
|
||||
assert.deepEqual(resolveDefaultAuthorizedRoute(adminUser), { name: 'app-overview' })
|
||||
assert.deepEqual(resolveDefaultAuthorizedRoute(adminUser), { name: 'app-documents' })
|
||||
assert.deepEqual(
|
||||
filterNavItemsByAccess(navItems, adminUser).map((item) => item.id),
|
||||
['documents', 'overview', 'settings']
|
||||
@@ -201,6 +201,30 @@ test('direct-manager approval helpers only match claims pushed to the current us
|
||||
assert.equal(isCurrentDirectManagerForRequest({ person: '张三', managerName: '王总' }, managerUser), false)
|
||||
})
|
||||
|
||||
test('approver executive users can process claims routed to their direct-manager identity', () => {
|
||||
const leaderUser = {
|
||||
roleCodes: ['approver', 'executive'],
|
||||
name: 'Xiang Wanhong',
|
||||
username: 'xiangwanhong@xf.com'
|
||||
}
|
||||
|
||||
assert.equal(canApproveLeaderExpenseClaims(leaderUser), true)
|
||||
assert.equal(
|
||||
isCurrentDirectManagerForRequest(
|
||||
{ person: 'Shen Zhiyuan', managerName: 'Xiang Wanhong' },
|
||||
leaderUser
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.equal(
|
||||
isCurrentDirectManagerForRequest(
|
||||
{ person: 'Xiang Wanhong', managerName: 'Li Wenjing' },
|
||||
leaderUser
|
||||
),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('applicant helper matches generated draft owner by employee identifiers', () => {
|
||||
const currentUser = {
|
||||
username: 'caoxiaozhu@xf.com',
|
||||
|
||||
@@ -27,6 +27,10 @@ const appShellComposable = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/useAppShell.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const requestsComposable = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const assistantScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -70,6 +74,47 @@ test('documents center reloads immediately when entered or clicked again', () =>
|
||||
assert.match(appShellComposable, /reloadDocumentCenterRequests,/)
|
||||
})
|
||||
|
||||
test('documents center uses the full request list instead of the global date-filtered list', () => {
|
||||
assert.match(
|
||||
appShellRouteView,
|
||||
/<DocumentsCenterView[\s\S]*:filtered-requests="requests"[\s\S]*:has-data="requests\.length > 0"/
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
appShellRouteView,
|
||||
/<DocumentsCenterView[\s\S]*:filtered-requests="filteredRequests"/
|
||||
)
|
||||
})
|
||||
|
||||
test('workbench summary merges approval inbox requests without polluting document center rows', () => {
|
||||
assert.match(appShellComposable, /import \{ fetchAllApprovalExpenseClaims, fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
|
||||
assert.match(appShellComposable, /const workbenchApprovalRequests = ref\(\[\]\)/)
|
||||
assert.match(appShellComposable, /async function reloadWorkbenchApprovalRequests\(\)/)
|
||||
assert.match(appShellComposable, /async function reloadWorkbenchRequests\(\)/)
|
||||
assert.match(appShellComposable, /fetchAllApprovalExpenseClaims\(\)/)
|
||||
assert.match(appShellComposable, /payload\.map\(\(item\) => mapExpenseClaimToRequest\(item\)\)/)
|
||||
assert.match(appShellComposable, /Promise\.all\(\[[\s\S]*reloadRequests\(\{ silent: true \}\),[\s\S]*reloadWorkbenchApprovalRequests\(\)[\s\S]*\]\)/)
|
||||
assert.match(appShellComposable, /if \(view === 'workbench'\) \{[\s\S]*void reloadWorkbenchRequests\(\)/)
|
||||
assert.match(appShellComposable, /const workbenchRequests = computed\(\(\) =>[\s\S]*mergeWorkbenchRequests\(requests\.value, workbenchApprovalRequests\.value\)/)
|
||||
assert.match(appShellComposable, /buildWorkbenchSummary\(workbenchRequests\.value, currentUser\.value\)/)
|
||||
assert.match(appShellRouteView, /<DocumentsCenterView[\s\S]*:filtered-requests="requests"/)
|
||||
assert.doesNotMatch(appShellRouteView, /<DocumentsCenterView[\s\S]*workbenchRequests/)
|
||||
})
|
||||
|
||||
test('workbench progress refreshes after homepage create or detail updates', () => {
|
||||
assert.match(appShellComposable, /async function handleDraftSaved\(payload = \{\}\) \{[\s\S]*await reloadWorkbenchRequests\(\)/)
|
||||
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadWorkbenchRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
|
||||
assert.doesNotMatch(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadRequests\(\)[\s\S]*await refreshSelectedRequestDetail/)
|
||||
})
|
||||
|
||||
test('workbench progress refresh is silent to avoid homepage flashing', () => {
|
||||
assert.match(requestsComposable, /async function reload\(options = \{\}\) \{[\s\S]*const silent = Boolean\(options\?\.silent\)/)
|
||||
assert.match(requestsComposable, /if \(!silent\) \{[\s\S]*loading\.value = true[\s\S]*error\.value = ''[\s\S]*\}/)
|
||||
assert.match(requestsComposable, /catch \(nextError\) \{[\s\S]*if \(!silent\) \{[\s\S]*requests\.value = \[\][\s\S]*\}/)
|
||||
assert.match(requestsComposable, /finally \{[\s\S]*if \(!silent\) \{[\s\S]*loading\.value = false[\s\S]*\}/)
|
||||
assert.match(appShellComposable, /async function reloadWorkbenchRequests\(\) \{[\s\S]*reloadRequests\(\{ silent: true \}\)/)
|
||||
assert.match(appShellComposable, /async function reloadDocumentCenterRequests\(\) \{[\s\S]*return reloadRequests\(\)/)
|
||||
})
|
||||
|
||||
test('document detail navigation preserves document center list query', () => {
|
||||
assert.match(
|
||||
appShellComposable,
|
||||
@@ -86,12 +131,12 @@ test('document detail navigation preserves document center list query', () => {
|
||||
})
|
||||
|
||||
test('document detail refreshes claim detail instead of relying on stale list cache', () => {
|
||||
assert.match(appShellComposable, /import \{ fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
|
||||
assert.match(appShellComposable, /import \{ fetchAllApprovalExpenseClaims, fetchExpenseClaimDetail \} from '\.\.\/services\/reimbursements\.js'/)
|
||||
assert.match(appShellComposable, /import \{ mapExpenseClaimToRequest, useRequests \} from '\.\/useRequests\.js'/)
|
||||
assert.match(appShellComposable, /const snapshot = normalizeRequestForUi\(selectedRequestSnapshot\.value\)[\s\S]*if \(isSameRequestIdentity\(snapshot, requestId\)\) \{[\s\S]*return snapshot/)
|
||||
assert.match(appShellComposable, /async function refreshSelectedRequestDetail\(requestOrId = selectedRequestSnapshot\.value\) \{[\s\S]*fetchExpenseClaimDetail\(lookupId\)[\s\S]*mapExpenseClaimToRequest\(payload\)[\s\S]*upsertRequestSnapshot\(mappedRequest\)/)
|
||||
assert.match(appShellComposable, /function openRequestDetail\(request, options = \{\}\) \{[\s\S]*void refreshSelectedRequestDetail\(request\)/)
|
||||
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
|
||||
assert.match(appShellComposable, /async function handleRequestUpdated\(\) \{[\s\S]*await reloadWorkbenchRequests\(\)[\s\S]*await refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
|
||||
assert.match(appShellComposable, /route\.name === 'app-document-detail'[\s\S]*void refreshSelectedRequestDetail\(String\(route\.params\.requestId \|\| ''\)\)/)
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ const documentListSharedStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/components/document-list-shared.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const reimbursementService = readFileSync(
|
||||
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const requestsComposable = readFileSync(
|
||||
fileURLToPath(new URL('../src/composables/useRequests.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('documents center keeps only the top scope tabs and renders status as a dropdown filter', () => {
|
||||
assert.match(documentsCenterView, /<nav class="status-tabs document-scope-tabs"/)
|
||||
@@ -134,6 +142,17 @@ test('documents center refresh token reloads supporting approval and archive row
|
||||
assert.match(documentsCenterView, /function reloadAll\(\) \{[\s\S]*emit\('reload'\)[\s\S]*void loadSupportingRows\(\)/)
|
||||
})
|
||||
|
||||
test('documents center fetches every paginated claim page for admin-scale lists', () => {
|
||||
assert.match(reimbursementService, /export function fetchAllExpenseClaims/)
|
||||
assert.match(reimbursementService, /async function fetchAllExpenseClaimPages/)
|
||||
assert.match(reimbursementService, /payload\.has_next/)
|
||||
assert.match(requestsComposable, /import \{ fetchAllExpenseClaims \} from '\.\.\/services\/reimbursements\.js'/)
|
||||
assert.match(requestsComposable, /const payload = await fetchAllExpenseClaims\(\)/)
|
||||
assert.match(documentsCenterView, /fetchAllApprovalExpenseClaims/)
|
||||
assert.match(documentsCenterView, /fetchAllArchivedExpenseClaims/)
|
||||
assert.doesNotMatch(documentsCenterView, /REIMBURSEMENT_LIST_PREVIEW_PARAMS/)
|
||||
})
|
||||
|
||||
test('documents center list shows created time and conditional stay time columns', () => {
|
||||
assert.match(documentsCenterView, /import \{[\s\S]*formatDocumentListTime[\s\S]*resolveDocumentStayTimeDisplay[\s\S]*\} from '..\/utils\/documentCenterTime\.js'/)
|
||||
assert.match(documentsCenterView, /<col class="col-created">/)
|
||||
@@ -326,10 +345,9 @@ test('documents center status dropdown uses compact filter styling', () => {
|
||||
assert.match(documentsCenterStyles, /\.col-created\s*\{\s*width:\s*10%;\s*\}/)
|
||||
assert.match(documentsCenterStyles, /\.col-stay\s*\{\s*width:\s*9%;\s*\}/)
|
||||
assert.match(documentsCenterStyles, /\.col-initiator\s*\{\s*width:\s*8%;\s*\}/)
|
||||
assert.match(documentsCenterStyles, /\.document-status-filter\s*\{[\s\S]*display:\s*inline-flex;/)
|
||||
assert.match(documentsCenterStyles, /\.document-status-filter\s*\{[\s\S]*min-height:\s*38px;/)
|
||||
assert.match(documentsCenterStyles, /\.status-filter-trigger\s*\{[\s\S]*min-width:\s*154px;/)
|
||||
assert.match(documentsCenterStyles, /\.status-filter-menu\s*\{[\s\S]*min-width:\s*154px;/)
|
||||
assert.match(documentListSharedStyles, /\.document-status-filter\s*\{[\s\S]*display:\s*inline-flex;/)
|
||||
assert.match(documentListSharedStyles, /\.document-status-filter\s*\{[\s\S]*min-height:\s*38px;/)
|
||||
assert.match(documentListSharedStyles, /\.status-dropdown-filter,\s*\.status-filter-trigger,\s*\.status-filter-menu\s*\{[\s\S]*min-width:\s*154px;/)
|
||||
assert.doesNotMatch(documentsCenterStyles, /\.document-state-tabs\s*\{/)
|
||||
assert.doesNotMatch(documentsCenterStyles, /\.document-status-filter\s*\{[^}]*margin-top:/)
|
||||
})
|
||||
|
||||
@@ -22,6 +22,18 @@ test('expense profile modal remounts the behavior radar when opened', () => {
|
||||
assert.match(modal, /scheduleRadarFrame/)
|
||||
})
|
||||
|
||||
test('expense profile modal uses compact laptop dialog sizing', () => {
|
||||
assert.match(modal, /width="min\(960px, calc\(100vw - 64px\)\)"/)
|
||||
assert.match(modal, /max-height:\s*min\(580px, calc\(100dvh - 176px\)\)/)
|
||||
assert.match(
|
||||
modal,
|
||||
/@media \(min-width: 861px\) and \(max-width: 1440px\),\s*\n\s*\(min-width: 861px\) and \(max-height: 820px\)/
|
||||
)
|
||||
assert.match(modal, /width:\s*min\(900px, calc\(100vw - 96px\)\) !important;/)
|
||||
assert.match(modal, /max-height:\s*min\(520px, calc\(100dvh - 152px\)\)/)
|
||||
assert.match(modal, /\.profile-radar-chart \{[\s\S]*height:\s*248px;/)
|
||||
})
|
||||
|
||||
test('radar chart uses the shared echarts lifecycle and enables entrance animation', () => {
|
||||
assert.match(radarChart, /import \{ useEcharts \} from '\.\.\/\.\.\/composables\/useEcharts\.js'/)
|
||||
assert.match(radarChart, /useEcharts\(chartElement, chartOptions\)/)
|
||||
|
||||
@@ -30,6 +30,15 @@ test('expense stats detail modal exposes distribution, processing time and opera
|
||||
assert.match(modal, /resolveDistributionColor/)
|
||||
assert.match(modal, /processingRows/)
|
||||
assert.match(modal, /operationRows/)
|
||||
assert.match(modal, /width="min\(900px, calc\(100vw - 64px\)\)"/)
|
||||
assert.match(modal, /max-height:\s*min\(580px, calc\(100dvh - 176px\)\)/)
|
||||
assert.match(
|
||||
modal,
|
||||
/@media \(min-width: 861px\) and \(max-width: 1440px\),\s*\n\s*\(min-width: 861px\) and \(max-height: 820px\)/
|
||||
)
|
||||
assert.match(modal, /width:\s*min\(860px, calc\(100vw - 96px\)\) !important;/)
|
||||
assert.match(modal, /max-height:\s*min\(520px, calc\(100dvh - 152px\)\)/)
|
||||
assert.match(modal, /\.expense-distribution-donut :deep\(\.donut-body\) \{[\s\S]*height:\s*170px;/)
|
||||
assert.doesNotMatch(modal, /--expense-detail-percent/)
|
||||
assert.doesNotMatch(modal, /expense-distribution-track/)
|
||||
assert.match(modal, /统计口径来自当前工作台已加载的个人单据/)
|
||||
|
||||
@@ -13,6 +13,14 @@ const workbench = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/business/PersonalWorkbench.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const workbenchProgressPanel = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/business/PersonalWorkbenchProgressPanel.vue', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const workbenchProgressStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/components/personal-workbench-progress.css', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const workbenchStyles = readFileSync(
|
||||
fileURLToPath(new URL('../src/assets/styles/components/personal-workbench.css', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -31,7 +39,7 @@ const workbenchInsightStyles = readFileSync(
|
||||
'utf8'
|
||||
)
|
||||
const heroBackgroundAsset = fileURLToPath(
|
||||
new URL('../src/assets/personal-workbench-hero-bg-theme-base.webp', import.meta.url)
|
||||
new URL('../src/assets/images/hero-3d-banner.png', import.meta.url)
|
||||
)
|
||||
const capabilityGlassAsset = fileURLToPath(
|
||||
new URL('../src/assets/personal-workbench-card-glass-capability.webp', import.meta.url)
|
||||
@@ -43,8 +51,9 @@ const panelGlassAsset = fileURLToPath(
|
||||
test('workbench assistant greets the current employee without the old helper tag', () => {
|
||||
assert.doesNotMatch(workbench, /assistant-tag/)
|
||||
assert.doesNotMatch(workbench, /AI 报销助手/)
|
||||
assert.match(workbench, /嗨,\{\{ displayUserName \}\},我是您的 <span>AI 费用助手<\/span>/)
|
||||
assert.match(workbench, /placeholder="请输入费用申请、报销问题、预算查询或制度问答\.\.\."/)
|
||||
assert.match(workbench, /\{\{ typedTitlePrefix \}\}<span v-if="titleTypingDone">小财管家<\/span>/)
|
||||
assert.match(workbench, /const heroTitleText = computed\(\(\) => `嗨,\$\{displayUserName\.value\},我是您的 `\)/)
|
||||
assert.match(workbench, /placeholder="一次性描述申请、报销和附件处理事项,小财管家会先拆解再执行\.\.\."/)
|
||||
assert.match(workbench, /const displayUserName = computed/)
|
||||
assert.match(workbench, /user\.name/)
|
||||
})
|
||||
@@ -83,16 +92,16 @@ test('workbench capability cards keep user-entered context only', () => {
|
||||
})
|
||||
|
||||
test('workbench hero uses theme-tintable background image', () => {
|
||||
assert.match(workbench, /personal-workbench-hero-bg-theme-base\.webp/)
|
||||
assert.doesNotMatch(workbench, /personal-workbench-hero-bg-theme-base\.png/)
|
||||
assert.match(workbench, /hero-3d-banner\.png/)
|
||||
assert.doesNotMatch(workbench, /personal-workbench-hero-bg-theme-base\.(webp|png)/)
|
||||
assert.match(workbench, /--assistant-bg-image.*workbenchHeroBackground/)
|
||||
assert.match(workbenchStyles, /--assistant-theme-tint:[\s\S]*--theme-primary-rgb/)
|
||||
assert.match(workbenchStyles, /var\(--assistant-bg-image\) var\(--assistant-bg-position\) \/ var\(--assistant-bg-size\) no-repeat/)
|
||||
assert.match(workbenchStyles, /background-blend-mode:\s*normal,\s*color,\s*luminosity;/)
|
||||
assert.match(workbenchStyles, /\.assistant-hero::after\s*\{[\s\S]*content:\s*none;/)
|
||||
assert.match(workbenchResponsiveStyles, /--assistant-bg-position:\s*68% center;/)
|
||||
assert.match(workbenchStyles, /url\("\.\.\/\.\.\/images\/workbench-hero-right-bg\.png"\) var\(--assistant-bg-position\) \/ var\(--assistant-decor-width\) auto no-repeat/)
|
||||
assert.match(workbenchStyles, /\.assistant-hero::after\s*\{[\s\S]*content:\s*"";/)
|
||||
assert.match(workbenchResponsiveStyles, /--assistant-bg-position:\s*right center;/)
|
||||
assert.doesNotMatch(workbenchResponsiveStyles, /homepage_backgraound/)
|
||||
assert.ok(statSync(heroBackgroundAsset).size < 120 * 1024)
|
||||
assert.ok(statSync(heroBackgroundAsset).size > 1024)
|
||||
assert.ok(statSync(heroBackgroundAsset).size < 600 * 1024)
|
||||
})
|
||||
|
||||
test('workbench cards use layered glass material instead of texture-led cards', () => {
|
||||
@@ -103,13 +112,11 @@ test('workbench cards use layered glass material instead of texture-led cards',
|
||||
assert.match(workbenchGlassStyles, /--workbench-glass-highlight:/)
|
||||
assert.match(workbenchGlassStyles, /--workbench-glass-noise-opacity:\s*0\.012;/)
|
||||
assert.match(workbenchGlassStyles, /--workbench-glass-blur:\s*blur\(18px\) saturate\(1\.28\);/)
|
||||
assert.match(workbenchGlassStyles, /\.capability-card\s*\{[\s\S]*background-color:\s*rgba\(255,\s*255,\s*255,\s*0\.64\);[\s\S]*backdrop-filter:\s*var\(--workbench-glass-blur\)/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card\s*\{[\s\S]*background-color:\s*rgba\(255,\s*255,\s*255,\s*0\.66\);[\s\S]*backdrop-filter:\s*var\(--workbench-glass-blur\)/)
|
||||
assert.match(workbenchGlassStyles, /\.capability-card\s*\{[\s\S]*background:\s*rgba\(255,\s*255,\s*255,\s*0\.96\);[\s\S]*backdrop-filter:\s*blur\(12px\) saturate\(150%\)/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card\s*\{[\s\S]*background:\s*rgba\(255,\s*255,\s*255,\s*0\.96\);[\s\S]*backdrop-filter:\s*blur\(12px\) saturate\(150%\)/)
|
||||
assert.match(workbenchGlassStyles, /\.capability-card::before,[\s\S]*\.capability-card::after/)
|
||||
assert.match(workbenchGlassStyles, /\.capability-card::before\s*\{[\s\S]*var\(--workbench-capability-bg-image\) 0 0 \/ var\(--workbench-capability-tile-size\) repeat;[\s\S]*opacity:\s*var\(--workbench-glass-noise-opacity\);/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card::before\s*\{[\s\S]*var\(--workbench-panel-bg-image\) 0 0 \/ var\(--workbench-panel-tile-size\) repeat;[\s\S]*opacity:\s*calc\(var\(--workbench-glass-noise-opacity\) \* 0\.8\);/)
|
||||
assert.match(workbenchGlassStyles, /\.capability-card::after\s*\{[\s\S]*var\(--workbench-glass-highlight\)/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card::after\s*\{[\s\S]*var\(--workbench-glass-highlight\)/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card::before,[\s\S]*\.workbench-card::after\s*\{[\s\S]*display:\s*none !important;/)
|
||||
assert.doesNotMatch(workbenchGlassStyles, /\.capability-card::after\s*\{[^}]*radial-gradient/)
|
||||
assert.doesNotMatch(workbenchGlassStyles, /\.workbench-card::after\s*\{[^}]*radial-gradient/)
|
||||
assert.match(workbenchGlassStyles, /\.workbench-card > \*\s*\{[\s\S]*z-index:\s*1;/)
|
||||
@@ -143,29 +150,67 @@ test('workbench submit shows intent recognition feedback before assistant opens'
|
||||
assert.match(workbench, /:readonly="isComposerPending"/)
|
||||
})
|
||||
|
||||
test('workbench progress rows show update time first', () => {
|
||||
assert.match(workbench, /class="progress-time"/)
|
||||
assert.match(workbench, /class="progress-type"/)
|
||||
assert.match(workbench, /费用类型/)
|
||||
assert.match(workbench, /\{\{ item\.expenseTypeLabel \|\| '其他费用' \}\}/)
|
||||
test('workbench document progress has range filter, document types and empty state', () => {
|
||||
assert.match(workbench, /import PersonalWorkbenchProgressPanel from '\.\/PersonalWorkbenchProgressPanel\.vue'/)
|
||||
assert.match(workbench, /<PersonalWorkbenchProgressPanel[\s\S]*:progress-items="workbenchSummary\.progressItems \|\| \[\]"[\s\S]*@open-target="openWorkbenchTarget"/)
|
||||
assert.match(workbenchProgressPanel, /<h2>单据进度<\/h2>/)
|
||||
assert.match(workbenchProgressPanel, /personal-workbench-progress\.css/)
|
||||
assert.match(workbenchProgressPanel, /EnterpriseSelect/)
|
||||
assert.match(workbenchProgressPanel, /近10日/)
|
||||
assert.match(workbenchProgressPanel, /近30日/)
|
||||
assert.match(workbenchProgressPanel, /近3个月/)
|
||||
assert.match(workbenchProgressPanel, /class="progress-range-control"[\s\S]*@click\.stop[\s\S]*@mousedown\.stop[\s\S]*@pointerdown\.stop/)
|
||||
assert.match(workbenchProgressPanel, /:teleported="true"/)
|
||||
assert.match(workbenchProgressPanel, /@click="handleProgressItemClick\(\$event, item\)"/)
|
||||
assert.match(workbenchProgressPanel, /function handleProgressItemClick\(event, item\)/)
|
||||
assert.match(workbenchProgressPanel, /return true/)
|
||||
assert.match(workbenchProgressPanel, /class="progress-time"/)
|
||||
assert.match(workbenchProgressPanel, /class="progress-applicant"/)
|
||||
assert.match(workbenchProgressPanel, /class="progress-table-shell"/)
|
||||
assert.match(workbenchProgressPanel, />\u66f4\u65b0\u65f6\u95f4<\/span>/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /\u5355\u636e\u52a8\u6001/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /time-capsule/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /<small>\u7533\u8bf7\u4eba<\/small>/)
|
||||
assert.match(workbenchProgressPanel, /\{\{ item\.applicantLabel \|\| '待补充' \}\}/)
|
||||
assert.match(workbenchProgressPanel, /class="progress-type"/)
|
||||
assert.match(workbenchProgressPanel, /\{\{ item\.documentTypeLabel \}\} · \{\{ item\.expenseTypeLabel \|\| '其他费用' \}\}/)
|
||||
assert.match(workbenchProgressPanel, /当前范围暂无单据/)
|
||||
assert.match(workbenchProgressPanel, /没有申请单或报销单进度/)
|
||||
assert.match(workbench, /source:\s*'workbench'/)
|
||||
assert.match(workbench, /returnTo:\s*'workbench'/)
|
||||
assert.match(workbench, /has-long-duration-divider/)
|
||||
assert.match(workbench, /hasLongDurationDivider/)
|
||||
assert.match(workbench, /const LONG_DURATION_DAYS = 10/)
|
||||
assert.match(workbench, /isLongDurationProgress\(item\?\.updatedAt\)/)
|
||||
assert.match(workbench, /<time :datetime="item\.updatedAt \|\| ''">\{\{ item\.displayTime \}\}<\/time>/)
|
||||
assert.match(workbench, /displayTime: formatProgressTime\(item\?\.updatedAt\)/)
|
||||
assert.match(workbench, /function formatProgressTime\(value\)/)
|
||||
assert.doesNotMatch(workbench, />全部进度/)
|
||||
assert.match(workbenchStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*minmax\(78px,\s*0\.42fr\)[\s\S]*minmax\(84px,\s*0\.42fr\)/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /has-long-duration-divider/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /LONG_DURATION_DAYS/)
|
||||
assert.doesNotMatch(workbenchProgressPanel, /10日以上/)
|
||||
assert.doesNotMatch(workbenchStyles, /10日以上/)
|
||||
assert.doesNotMatch(workbenchStyles, /has-long-duration-divider/)
|
||||
assert.doesNotMatch(workbench, />费用进度</)
|
||||
assert.match(workbenchProgressPanel, /<time :datetime="item\.updatedAt \|\| ''">\{\{ item\.displayTime \}\}<\/time>/)
|
||||
assert.match(workbenchProgressPanel, /function formatProgressTime\(value\)/)
|
||||
assert.match(workbenchStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*minmax\(118px,\s*0\.58fr\)[\s\S]*minmax\(84px,\s*0\.42fr\)/)
|
||||
assert.match(workbenchStyles, /\.progress-type\s*\{[\s\S]*justify-self:\s*stretch;[\s\S]*justify-items:\s*center;[\s\S]*text-align:\s*center;/)
|
||||
assert.match(workbenchStyles, /\.progress-type strong\s*\{[\s\S]*justify-content:\s*center;/)
|
||||
assert.match(workbenchStyles, /\.progress-type strong\s*\{[\s\S]*var\(--workbench-primary-active\)/)
|
||||
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*content:\s*"10日以上"/)
|
||||
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*left:\s*50%;[\s\S]*transform:\s*translateX\(-50%\);/)
|
||||
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::before\s*\{[\s\S]*color:\s*var\(--theme-primary-active\);/)
|
||||
assert.match(workbenchStyles, /\.progress-row\.has-long-duration-divider::after\s*\{[\s\S]*rgba\(var\(--theme-primary-rgb/)
|
||||
assert.match(workbenchStyles, /\.progress-list\s*\{[\s\S]*overflow-y:\s*auto;/)
|
||||
assert.match(workbenchStyles, /\.progress-empty-state\s*\{[\s\S]*border:\s*1px dashed/)
|
||||
assert.match(workbenchStyles, /\.progress-range-select\s*\{[\s\S]*width:\s*124px;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-panel\s*\{[\s\S]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);/)
|
||||
assert.match(workbenchProgressStyles, /--progress-table-columns:[\s\S]*minmax\(96px,\s*0\.44fr\);/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-table-shell\s*\{[\s\S]*grid-template-rows:\s*36px minmax\(0,\s*1fr\);[\s\S]*overflow:\s*hidden;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-table-header\s*\{[\s\S]*grid-template-columns:\s*var\(--progress-table-columns\);/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-table-header\s*\{[\s\S]*height:\s*36px;[\s\S]*max-height:\s*36px;[\s\S]*overflow:\s*hidden;/)
|
||||
assert.match(workbenchProgressStyles, /\.header-cell\s*\{[\s\S]*justify-content:\s*center;[\s\S]*white-space:\s*nowrap;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-list\s*\{[\s\S]*grid-auto-rows:\s*minmax\(76px,\s*auto\)/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-row\s*\{[\s\S]*grid-template-columns:\s*var\(--progress-table-columns\);[\s\S]*gap:\s*8px;[\s\S]*min-height:\s*76px;[\s\S]*text-align:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-time-wrapper\s*\{[\s\S]*justify-content:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-identity\s*\{[\s\S]*align-items:\s*center;[\s\S]*text-align:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-applicant\s*\{[\s\S]*justify-items:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-result\s*\{[\s\S]*justify-self:\s*stretch;[\s\S]*justify-content:\s*center;[\s\S]*justify-items:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-result strong\s*\{[\s\S]*width:\s*100%;[\s\S]*text-align:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-steps\s*\{[\s\S]*justify-items:\s*stretch;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-step\s*\{[\s\S]*justify-items:\s*center;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-range-control\s*\{[\s\S]*z-index:\s*6;/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-range-select:deep\(\.el-select__wrapper\)/)
|
||||
assert.match(workbenchProgressStyles, /\.progress-empty-state\s*\{[\s\S]*border:\s*1px dashed/)
|
||||
assert.match(workbenchStyles, /\.progress-time\s*\{[\s\S]*color:\s*var\(--workbench-muted\);/)
|
||||
assert.match(workbenchResponsiveStyles, /grid-template-areas:[\s\S]*"time identity result"[\s\S]*"type type type"[\s\S]*"steps steps steps"/)
|
||||
assert.match(workbenchResponsiveStyles, /\.progress-type\s*\{[\s\S]*grid-area:\s*type;/)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
const CREATE_APPLICATION = '\u521b\u5efa\u7533\u8bf7'
|
||||
const DIRECT_MANAGER_APPROVAL = '\u76f4\u5c5e\u9886\u5bfc\u5ba1\u6279'
|
||||
const BUDGET_MANAGER_APPROVAL = '\u9884\u7b97\u7ba1\u7406\u8005\u5ba1\u6279'
|
||||
const FINANCE_APPROVAL = '\u8d22\u52a1\u5ba1\u6279'
|
||||
const APPROVAL_COMPLETED = '\u5ba1\u6279\u5b8c\u6210'
|
||||
const APPLICATION_LINK_STATUS = '\u5173\u8054\u5355\u636e\u72b6\u6001'
|
||||
const APPLICATION_ARCHIVE = '\u7533\u8bf7\u5f52\u6863'
|
||||
@@ -21,6 +22,7 @@ const ARCHIVED = '\u5df2\u5f52\u6863'
|
||||
const WAIT_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \u6279\u590d'
|
||||
const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d'
|
||||
const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d'
|
||||
const WAIT_FINANCE_FIONA_APPROVAL = '\u7b49\u5f85 Fiona Finance \u6279\u590d'
|
||||
const LEADER_RETURNED_STATUS = '\u9886\u5bfc\u5df2\u9000\u56de\uff0c\u5f85\u91cd\u65b0\u63d0\u4ea4'
|
||||
|
||||
test('claim mapper exposes employee identifier for reviewer risk profile lookup', () => {
|
||||
@@ -158,7 +160,7 @@ 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, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
|
||||
@@ -252,7 +254,7 @@ test('application claims wait for department P8 budget monitor after leader appr
|
||||
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_ZHAO_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_ZHAO_APPROVAL)?.current, true)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
|
||||
@@ -295,7 +297,7 @@ test('application budget wait label uses claim-level budget approver snapshot',
|
||||
assert.equal(request.budgetApproverName, 'P8 Executive')
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, WAIT_BUDGET_P8_EXECUTIVE_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_BUDGET_P8_EXECUTIVE_APPROVAL)?.current, true)
|
||||
})
|
||||
@@ -391,7 +393,7 @@ test('approved application claims complete after budget approval', () => {
|
||||
assert.equal(request.workflowNode, APPLICATION_LINK_STATUS)
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, BUDGET_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.time, '未关联')
|
||||
@@ -417,15 +419,40 @@ test('application claims hide budget step when leader approval also covers budge
|
||||
created_at: '2026-05-25T01:30:00.000Z',
|
||||
updated_at: '2026-05-25T03:00:00.000Z',
|
||||
status: 'approved',
|
||||
approval_stage: APPROVAL_COMPLETED,
|
||||
approval_stage: APPLICATION_LINK_STATUS,
|
||||
risk_flags_json: [
|
||||
{
|
||||
source: 'approval_routing',
|
||||
event_type: 'expense_application_route_decision',
|
||||
requires_budget_review: true,
|
||||
route: 'budget_manager',
|
||||
budget_result: {
|
||||
metrics: {
|
||||
after_usage_rate: '99.27',
|
||||
claim_amount_ratio: '1.27',
|
||||
over_budget_amount: '0.00'
|
||||
}
|
||||
},
|
||||
created_at: '2026-05-25T03:00:00.000Z'
|
||||
},
|
||||
{
|
||||
source: 'manual_approval',
|
||||
event_type: 'expense_application_approval',
|
||||
operator: '李预算经理',
|
||||
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
|
||||
next_approval_stage: APPROVAL_COMPLETED,
|
||||
next_approval_stage: APPLICATION_LINK_STATUS,
|
||||
budget_approval_merged: true,
|
||||
route_decision: {
|
||||
requires_budget_review: true,
|
||||
route: 'budget_manager',
|
||||
budget_result: {
|
||||
metrics: {
|
||||
after_usage_rate: '99.27',
|
||||
claim_amount_ratio: '1.27',
|
||||
over_budget_amount: '0.00'
|
||||
}
|
||||
}
|
||||
},
|
||||
created_at: '2026-05-25T03:00:00.000Z'
|
||||
}
|
||||
],
|
||||
@@ -434,7 +461,7 @@ test('application claims hide budget step when leader approval also covers budge
|
||||
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
|
||||
@@ -486,7 +513,7 @@ test('approved application claims hide budget step when dynamic route skipped bu
|
||||
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === APPLICATION_LINK_STATUS)?.current, true)
|
||||
assert.equal(request.progressSteps.find((step) => step.label === ARCHIVED)?.done, false)
|
||||
@@ -494,6 +521,69 @@ test('approved application claims hide budget step when dynamic route skipped bu
|
||||
assert.equal(request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)?.time, 'Leader Li通过')
|
||||
})
|
||||
|
||||
test('approved application claims hide stale budget route below 90 percent threshold', () => {
|
||||
const request = mapExpenseClaimToRequest({
|
||||
id: 'claim-application-stale-budget-route',
|
||||
claim_no: 'APP-20260525-STALE-BUDGET',
|
||||
employee_name: '张三',
|
||||
department_name: '交付部',
|
||||
manager_name: 'Leader Li',
|
||||
expense_type: 'travel_application',
|
||||
reason: 'Project onsite support',
|
||||
location: 'Shanghai',
|
||||
amount: 8500,
|
||||
invoice_count: 0,
|
||||
occurred_at: '2026-05-25T00:00:00.000Z',
|
||||
submitted_at: '2026-05-25T02:00:00.000Z',
|
||||
created_at: '2026-05-25T01:30:00.000Z',
|
||||
updated_at: '2026-05-25T03:00:00.000Z',
|
||||
status: 'approved',
|
||||
approval_stage: APPROVAL_COMPLETED,
|
||||
risk_flags_json: [
|
||||
{
|
||||
source: 'approval_routing',
|
||||
event_type: 'expense_application_route_decision',
|
||||
requires_budget_review: true,
|
||||
route: 'budget_manager',
|
||||
budget_result: {
|
||||
metrics: {
|
||||
after_usage_rate: '85.00',
|
||||
claim_amount_ratio: '85.00',
|
||||
over_budget_amount: '0.00'
|
||||
}
|
||||
},
|
||||
created_at: '2026-05-25T03:00:00.000Z'
|
||||
},
|
||||
{
|
||||
source: 'manual_approval',
|
||||
event_type: 'expense_application_approval',
|
||||
operator: 'Leader Li',
|
||||
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
|
||||
next_approval_stage: BUDGET_MANAGER_APPROVAL,
|
||||
route_decision: {
|
||||
requires_budget_review: true,
|
||||
route: 'budget_manager',
|
||||
budget_result: {
|
||||
metrics: {
|
||||
after_usage_rate: '85.00',
|
||||
claim_amount_ratio: '85.00',
|
||||
over_budget_amount: '0.00'
|
||||
}
|
||||
}
|
||||
},
|
||||
created_at: '2026-05-25T03:00:00.000Z'
|
||||
}
|
||||
],
|
||||
items: []
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
|
||||
})
|
||||
|
||||
test('approved application claims show linked reimbursement status before archive', () => {
|
||||
const request = mapExpenseClaimToRequest({
|
||||
id: 'claim-application-linked-draft',
|
||||
@@ -566,7 +656,7 @@ test('application claims are archived only after linked reimbursement is paid',
|
||||
assert.equal(request.workflowNode, APPLICATION_ARCHIVE)
|
||||
assert.deepEqual(
|
||||
request.progressSteps.map((step) => step.label),
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPROVAL_COMPLETED, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
[CREATE_APPLICATION, DIRECT_MANAGER_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
||||
)
|
||||
assert.equal(request.progressSteps.every((step) => step.done), true)
|
||||
assert.equal(request.secondaryStatusValue, '已归档')
|
||||
@@ -582,6 +672,8 @@ test('progress steps show approval operator time and current stay duration', ()
|
||||
claim_no: 'EXP-202605-001',
|
||||
employee_name: '张三',
|
||||
department_name: '市场部',
|
||||
finance_owner_name: 'Wang Finance Group',
|
||||
finance_approver_name: 'Fiona Finance',
|
||||
expense_type: 'transport',
|
||||
reason: '交通报销',
|
||||
location: '上海',
|
||||
@@ -592,21 +684,21 @@ test('progress steps show approval operator time and current stay duration', ()
|
||||
created_at: '2026-05-20T01:30:00.000Z',
|
||||
updated_at: '2026-05-20T03:30:00.000Z',
|
||||
status: 'submitted',
|
||||
approval_stage: '财务审批',
|
||||
approval_stage: FINANCE_APPROVAL,
|
||||
risk_flags_json: [
|
||||
{
|
||||
source: 'manual_approval',
|
||||
operator: '李经理',
|
||||
previous_approval_stage: '直属领导审批',
|
||||
next_approval_stage: '财务审批',
|
||||
previous_approval_stage: DIRECT_MANAGER_APPROVAL,
|
||||
next_approval_stage: FINANCE_APPROVAL,
|
||||
created_at: '2026-05-20T03:30:00.000Z'
|
||||
}
|
||||
],
|
||||
items: []
|
||||
})
|
||||
|
||||
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
|
||||
const financeStep = request.progressSteps.find((step) => step.label === '财务审批')
|
||||
const leaderStep = request.progressSteps.find((step) => step.label === DIRECT_MANAGER_APPROVAL)
|
||||
const financeStep = request.progressSteps.find((step) => step.rawLabel === FINANCE_APPROVAL)
|
||||
const firstStep = request.progressSteps[0]
|
||||
|
||||
assert.equal(request.riskSummary, '无')
|
||||
@@ -615,6 +707,10 @@ test('progress steps show approval operator time and current stay duration', ()
|
||||
assert.match(leaderStep.detail, /2026-05-20/)
|
||||
assert.match(leaderStep.title, /李经理审批通过/)
|
||||
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
|
||||
assert.equal(request.financeOwnerName, 'Wang Finance Group')
|
||||
assert.equal(request.financeApproverName, 'Fiona Finance')
|
||||
assert.equal(financeStep.label, WAIT_FINANCE_FIONA_APPROVAL)
|
||||
assert.equal(financeStep.rawLabel, FINANCE_APPROVAL)
|
||||
assert.equal(financeStep.current, true)
|
||||
assert.equal(financeStep.time, '停留 1小时30分钟')
|
||||
} finally {
|
||||
@@ -1124,6 +1220,7 @@ test('current direct manager step shows how long the claim has stayed there', ()
|
||||
claim_no: 'EXP-202605-002',
|
||||
employee_name: '王五',
|
||||
department_name: '市场部',
|
||||
manager_name: '李经理',
|
||||
expense_type: 'office',
|
||||
reason: '办公用品',
|
||||
location: '上海',
|
||||
@@ -1139,11 +1236,13 @@ test('current direct manager step shows how long the claim has stayed there', ()
|
||||
items: []
|
||||
})
|
||||
|
||||
const leaderStep = request.progressSteps.find((step) => step.label === '直属领导审批')
|
||||
const leaderStep = request.progressSteps.find((step) => step.rawLabel === '直属领导审批')
|
||||
const submitStep = request.progressSteps.find((step) => step.label === '待提交')
|
||||
|
||||
assert.equal(submitStep.time, '王五提交')
|
||||
assert.match(submitStep.detail, /2026-05-20/)
|
||||
assert.equal(leaderStep.label, '等待 李经理 批复')
|
||||
assert.equal(leaderStep.rawLabel, '直属领导审批')
|
||||
assert.equal(leaderStep.current, true)
|
||||
assert.equal(leaderStep.time, '停留 3小时15分钟')
|
||||
} finally {
|
||||
|
||||
@@ -67,6 +67,7 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
assert.match(detailScript, /const isCurrentDirectManagerApprover = computed/)
|
||||
assert.match(detailScript, /const canProcessFinanceApprovalStage = computed/)
|
||||
assert.match(detailScript, /const canProcessBudgetApprovalStage = computed/)
|
||||
assert.match(detailScript, /const canProcessCurrentApprovalStage = computed/)
|
||||
assert.match(detailScript, /approvalOpinionTitle/)
|
||||
assert.match(detailScript, /approvalConfirmDescription/)
|
||||
assert.doesNotMatch(detailScript, /approvalNextStage/)
|
||||
@@ -84,9 +85,14 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
||||
/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, /if \(isDirectManagerApprovalStage\.value\) \{[\s\S]*return isCurrentDirectManagerApprover\.value/)
|
||||
assert.match(detailScript, /canProcessFinanceApprovalStage\.value/)
|
||||
assert.match(detailScript, /canProcessBudgetApprovalStage\.value/)
|
||||
assert.match(detailScript, /const canApproveRequest = computed\(\(\) =>\s*request\.value\.approvalKey === 'in_progress'[\s\S]*&& canProcessCurrentApprovalStage\.value\s*\)/)
|
||||
assert.doesNotMatch(
|
||||
detailScript,
|
||||
/const canApproveRequest = computed\(\(\) =>\s*\(Boolean\(props\.approvalMode\) \|\| isApplicationDocument\.value\)/
|
||||
)
|
||||
assert.doesNotMatch(detailScript, /leaderApprovalReadonlyText/)
|
||||
assert.match(detailScript, /resolveGeneratedDraftClaimNo/)
|
||||
assert.match(detailScript, /resolveApproveErrorMessage/)
|
||||
|
||||
@@ -1169,6 +1169,28 @@ test('draft submit validation uses expense detail date and amount when claim sum
|
||||
assert.ok(!issues.some((issue) => issue.includes('报销事由未完善')))
|
||||
})
|
||||
|
||||
test('returned application submit validation does not require expense detail rows', () => {
|
||||
const issues = buildDraftBlockingIssues(
|
||||
{
|
||||
id: 'AP-202606060001-RETURNED',
|
||||
claimNo: 'AP-202606060001-RETURNED',
|
||||
documentTypeCode: 'application',
|
||||
approvalKey: 'rejected',
|
||||
profileName: '曹笑竹',
|
||||
typeLabel: '差旅费用申请',
|
||||
typeCode: 'travel_application',
|
||||
reason: '支撑国网仿生产环境部署',
|
||||
location: '上海',
|
||||
occurredDisplay: '2026-02-20 至 2026-02-23',
|
||||
amountValue: 1880
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
assert.deepEqual(issues, [])
|
||||
assert.equal(issues.includes('费用明细不能为空'), false)
|
||||
})
|
||||
|
||||
test('transport ticket descriptions use route format and invalid format becomes risk advice', () => {
|
||||
const routeItem = buildExpenseItemViewModel(
|
||||
{
|
||||
|
||||
@@ -71,6 +71,9 @@ test('workbench summary builds real user notifications and progress from request
|
||||
assert.equal(summary.todoItems.length, 1)
|
||||
assert.equal(summary.todoItems[0].target.id, 'claim-1')
|
||||
assert.equal(summary.progressItems.length, 1)
|
||||
assert.match(summary.progressItems[0].prompt, /单据进度/)
|
||||
assert.doesNotMatch(summary.progressItems[0].prompt, /费用进度/)
|
||||
assert.equal(summary.progressItems[0].applicantLabel, '张三')
|
||||
assert.deepEqual(
|
||||
summary.progressItems[0].steps.map((step) => step.label),
|
||||
['创建单据', '待提交', '直属领导审批', '财务审批']
|
||||
@@ -136,3 +139,65 @@ test('workbench progress keeps application document type for AP claims', () => {
|
||||
['申请单', '申请单']
|
||||
)
|
||||
})
|
||||
|
||||
test('workbench progress includes application rows without backend progress steps', () => {
|
||||
const summary = buildWorkbenchSummary(
|
||||
[
|
||||
{
|
||||
id: 'AP-202606060001-NOSTEPS',
|
||||
claimId: 'application-without-steps',
|
||||
claimNo: 'AP-202606060001-NOSTEPS',
|
||||
documentTypeCode: 'application',
|
||||
person: currentUser.name,
|
||||
title: '上海出差申请',
|
||||
approvalKey: 'in_progress',
|
||||
approvalStatus: '直属领导审批',
|
||||
amount: 1880,
|
||||
updatedAt: '2026-06-06T09:00:00+08:00'
|
||||
}
|
||||
],
|
||||
currentUser
|
||||
)
|
||||
|
||||
assert.equal(summary.progressItems.length, 1)
|
||||
assert.equal(summary.progressItems[0].documentTypeLabel, '申请单')
|
||||
assert.equal(summary.progressItems[0].applicantLabel, currentUser.name)
|
||||
assert.deepEqual(
|
||||
summary.progressItems[0].steps.map((step) => step.label),
|
||||
['创建申请', '直属领导审批', '关联单据状态', '已归档']
|
||||
)
|
||||
assert.equal(summary.progressItems[0].steps[1].current, true)
|
||||
})
|
||||
|
||||
test('workbench progress includes application documents assigned to current approver', () => {
|
||||
const approverUser = {
|
||||
name: '李经理',
|
||||
username: 'manager@example.com',
|
||||
roleCodes: ['approver']
|
||||
}
|
||||
const summary = buildWorkbenchSummary(
|
||||
[
|
||||
{
|
||||
id: 'AP-202606060002-APPROVAL',
|
||||
claimId: 'application-for-approver',
|
||||
claimNo: 'AP-202606060002-APPROVAL',
|
||||
documentTypeCode: 'application',
|
||||
person: '张三',
|
||||
managerName: '李经理',
|
||||
title: '北京出差申请',
|
||||
approvalKey: 'in_progress',
|
||||
approvalStatus: '直属领导审批',
|
||||
workflowNode: '直属领导审批',
|
||||
amount: 2600,
|
||||
updatedAt: '2026-06-06T11:00:00+08:00'
|
||||
}
|
||||
],
|
||||
approverUser
|
||||
)
|
||||
|
||||
assert.equal(summary.totalCount, 0)
|
||||
assert.equal(summary.progressItems.length, 1)
|
||||
assert.equal(summary.progressItems[0].documentTypeLabel, '申请单')
|
||||
assert.equal(summary.progressItems[0].applicantLabel, '张三')
|
||||
assert.equal(summary.progressItems[0].requestId, 'AP-202606060002-APPROVAL')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user