feat(expense): add persistent zero-entry receipt association
This commit is contained in:
@@ -410,8 +410,8 @@ export function useWorkbenchAiAttachmentAssociationFlow({
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
label: '查看单据',
|
||||
description: '打开已归集票据的报销单。',
|
||||
label: '查看草稿',
|
||||
description: '打开已归集票据的报销草稿。',
|
||||
icon: 'mdi mdi-open-in-new',
|
||||
action_type: 'open_application_detail',
|
||||
payload: {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import * as aiAttachmentAssociationModel from '../../utils/aiAttachmentAssociationModel.js'
|
||||
import { fetchAttachmentAssociationJob } from '../../services/attachmentAssociationJobs.js'
|
||||
import {
|
||||
buildAttachmentAssociationCandidateActions,
|
||||
isAttachmentAssociationConfirmationResult,
|
||||
resolveAttachmentAssociationTarget
|
||||
} from '../../utils/attachmentAssociationJobModel.js'
|
||||
import {
|
||||
fetchAttachmentAssociationJob,
|
||||
normalizeAttachmentAssociationJob
|
||||
} from '../../services/attachmentAssociationJobs.js'
|
||||
|
||||
const ATTACHMENT_ASSOCIATION_JOB_POLL_INTERVAL_MS = 1200
|
||||
const ATTACHMENT_ASSOCIATION_JOB_MAX_POLLS = 90
|
||||
@@ -25,23 +33,7 @@ export function useWorkbenchAiAttachmentAssociationJobs({
|
||||
}
|
||||
|
||||
function normalizeJob(job = {}) {
|
||||
const jobId = String(job?.job_id || job?.jobId || '').trim()
|
||||
if (!jobId) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
jobId,
|
||||
status: String(job?.status || 'queued').trim() || 'queued',
|
||||
message: String(job?.message || '').trim(),
|
||||
receiptIds: (Array.isArray(job?.receipt_ids) ? job.receipt_ids : job?.receiptIds || [])
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean),
|
||||
claimId: String(job?.claim_id || job?.claimId || '').trim(),
|
||||
claimNo: String(job?.claim_no || job?.claimNo || '').trim(),
|
||||
uploadedCount: Number(job?.uploaded_count ?? job?.uploadedCount ?? 0) || 0,
|
||||
skippedCount: Number(job?.skipped_count ?? job?.skippedCount ?? 0) || 0,
|
||||
error: String(job?.error || '').trim()
|
||||
}
|
||||
return normalizeAttachmentAssociationJob(job)
|
||||
}
|
||||
|
||||
function isPending(job = {}) {
|
||||
@@ -112,12 +104,39 @@ export function useWorkbenchAiAttachmentAssociationJobs({
|
||||
if (!findMessage(messageId)) {
|
||||
return true
|
||||
}
|
||||
if (isAttachmentAssociationConfirmationResult(normalizedJob)) {
|
||||
const confirmationMessage = aiAttachmentAssociationModel.buildAiAttachmentAssociationConfirmationResultMessage({
|
||||
job: normalizedJob,
|
||||
fileNames
|
||||
})
|
||||
await streamOrSetInlineAssistantContent(messageId, confirmationMessage)
|
||||
replaceJobMessage(messageId, confirmationMessage, {
|
||||
attachmentAssociationJob: normalizedJob,
|
||||
attachmentOcrDetails,
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: buildThinkingEvents('completed')
|
||||
},
|
||||
suggestedActions: buildAttachmentAssociationCandidateActions(normalizedJob)
|
||||
})
|
||||
persistCurrentConversation()
|
||||
return true
|
||||
}
|
||||
if (normalizedJob.status === 'succeeded') {
|
||||
const target = resolveAttachmentAssociationTarget(normalizedJob)
|
||||
const finalMessageText = aiAttachmentAssociationModel.buildAiAttachmentAssociationResultMessage({
|
||||
claimNo: normalizedJob.claimNo,
|
||||
claimNo: target.claimNo,
|
||||
fileNames,
|
||||
uploadedCount: normalizedJob.uploadedCount,
|
||||
skippedCount: normalizedJob.skippedCount
|
||||
skippedCount: normalizedJob.skippedCount,
|
||||
applicationClaimNo: normalizedJob.applicationClaimNo,
|
||||
confidence: normalizedJob.confidence,
|
||||
matchReasons: normalizedJob.matchReasons,
|
||||
riskItems: normalizedJob.riskItems,
|
||||
confirmationRequired: Boolean(
|
||||
normalizedJob.draftPayload?.confirmationRequired
|
||||
?? normalizedJob.draftPayload?.confirmation_required
|
||||
)
|
||||
})
|
||||
await streamOrSetInlineAssistantContent(messageId, finalMessageText)
|
||||
replaceJobMessage(messageId, finalMessageText, {
|
||||
@@ -128,13 +147,13 @@ export function useWorkbenchAiAttachmentAssociationJobs({
|
||||
thinkingEvents: buildThinkingEvents('completed')
|
||||
},
|
||||
suggestedActions: buildDetailActions({
|
||||
claimId: normalizedJob.claimId,
|
||||
claimNo: normalizedJob.claimNo
|
||||
claimId: target.claimId,
|
||||
claimNo: target.claimNo
|
||||
})
|
||||
})
|
||||
notifyRequestUpdated?.({
|
||||
claimId: normalizedJob.claimId,
|
||||
claimNo: normalizedJob.claimNo,
|
||||
claimId: target.claimId,
|
||||
claimNo: target.claimNo,
|
||||
source: 'ai-workbench-attachment-association-job',
|
||||
uploadedCount: normalizedJob.uploadedCount,
|
||||
skippedCount: normalizedJob.skippedCount
|
||||
@@ -244,7 +263,7 @@ export function useWorkbenchAiAttachmentAssociationJobs({
|
||||
extractReceiptIdsFromOcrDocuments,
|
||||
normalizeJob,
|
||||
pollJob,
|
||||
resumePendingJobs
|
||||
resumePendingJobs,
|
||||
updateJobMessage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeAttachmentAssociationJob } from '../../utils/attachmentAssociationJobModel.js'
|
||||
|
||||
export const AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION = 'confirm_ai_attachment_association'
|
||||
export const AI_ATTACHMENT_OCR_DETAIL_ACTION = 'show_ai_attachment_ocr_details'
|
||||
|
||||
@@ -212,28 +214,7 @@ export function createWorkbenchAiMessageRuntime() {
|
||||
}
|
||||
|
||||
export function normalizeInlineAttachmentAssociationJob(job = null) {
|
||||
if (!job || typeof job !== 'object') {
|
||||
return null
|
||||
}
|
||||
const jobId = String(job.jobId || job.job_id || '').trim()
|
||||
if (!jobId) {
|
||||
return null
|
||||
}
|
||||
const status = String(job.status || 'queued').trim() || 'queued'
|
||||
const receiptIds = (Array.isArray(job.receiptIds) ? job.receiptIds : job.receipt_ids || [])
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
return {
|
||||
jobId,
|
||||
status,
|
||||
message: String(job.message || '').trim(),
|
||||
receiptIds,
|
||||
claimId: String(job.claimId || job.claim_id || '').trim(),
|
||||
claimNo: String(job.claimNo || job.claim_no || '').trim(),
|
||||
uploadedCount: Number(job.uploadedCount ?? job.uploaded_count ?? 0) || 0,
|
||||
skippedCount: Number(job.skippedCount ?? job.skipped_count ?? 0) || 0,
|
||||
error: String(job.error || '').trim()
|
||||
}
|
||||
return normalizeAttachmentAssociationJob(job)
|
||||
}
|
||||
|
||||
export function normalizeInlineLinkedReimbursementDraftJob(job = null) {
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { apiRequest } from './api.js'
|
||||
import { normalizeAttachmentAssociationJob } from '../utils/attachmentAssociationJobModel.js'
|
||||
|
||||
export { normalizeAttachmentAssociationJob } from '../utils/attachmentAssociationJobModel.js'
|
||||
|
||||
function normalizeJobId(jobId) {
|
||||
return String(jobId || '').trim()
|
||||
}
|
||||
|
||||
export function createAttachmentAssociationJob(payload = {}) {
|
||||
return apiRequest('/reimbursements/attachment-association-jobs', {
|
||||
export async function createAttachmentAssociationJob(payload = {}) {
|
||||
const job = await apiRequest('/reimbursements/attachment-association-jobs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload || {})
|
||||
})
|
||||
return normalizeAttachmentAssociationJob(job)
|
||||
}
|
||||
|
||||
export function fetchAttachmentAssociationJob(jobId) {
|
||||
export async function fetchAttachmentAssociationJob(jobId) {
|
||||
const normalizedJobId = normalizeJobId(jobId)
|
||||
if (!normalizedJobId) {
|
||||
throw new Error('附件关联任务不存在或已失效。')
|
||||
}
|
||||
return apiRequest(`/reimbursements/attachment-association-jobs/${encodeURIComponent(normalizedJobId)}`)
|
||||
const job = await apiRequest(`/reimbursements/attachment-association-jobs/${encodeURIComponent(normalizedJobId)}`)
|
||||
return normalizeAttachmentAssociationJob(job)
|
||||
}
|
||||
|
||||
|
||||
@@ -449,25 +449,136 @@ export function buildAiAttachmentAssociationResultMessage({
|
||||
claimNo = '',
|
||||
uploadedCount = 0,
|
||||
skippedCount = 0,
|
||||
fileNames = []
|
||||
fileNames = [],
|
||||
applicationClaimNo = '',
|
||||
confidence = '',
|
||||
matchReasons = [],
|
||||
riskItems = [],
|
||||
confirmationRequired = false
|
||||
} = {}) {
|
||||
const normalizedUploadedCount = Math.max(0, Number(uploadedCount || 0))
|
||||
const normalizedSkippedCount = Math.max(0, Number(skippedCount || 0))
|
||||
const done = normalizedUploadedCount > 0 && normalizedSkippedCount === 0
|
||||
const alreadyAssociated = normalizedUploadedCount === 0 && normalizedSkippedCount > 0
|
||||
const completed = normalizedUploadedCount + normalizedSkippedCount > 0
|
||||
const riskText = formatAssociationIssueList(riskItems)
|
||||
const requiresReview = Boolean(confirmationRequired || riskText)
|
||||
const resultText = alreadyAssociated
|
||||
? `${normalizedSkippedCount} 份已关联,无需重复归集`
|
||||
: `${normalizedUploadedCount} 份新归集${normalizedSkippedCount ? `,${normalizedSkippedCount} 份已关联并跳过` : ''}`
|
||||
return [
|
||||
done ? '已完成自动归集。' : '自动归集已处理完成,请留意未归集附件。',
|
||||
alreadyAssociated
|
||||
? '票据此前已经完成归集,本次未重复写入。'
|
||||
: completed
|
||||
? '已完成自动归集。'
|
||||
: '自动归集未写入附件,请进入草稿核对。',
|
||||
renderAssociationCard({
|
||||
title: done ? '票据已归集' : '票据归集结果',
|
||||
status: done ? '已完成' : '部分完成',
|
||||
tone: done ? 'is-success' : 'is-warning',
|
||||
title: alreadyAssociated ? '票据已关联' : completed ? '票据已归集' : '票据归集结果',
|
||||
status: requiresReview ? '待复核' : completed ? '已完成' : '未完成',
|
||||
tone: requiresReview || !completed ? 'is-warning' : 'is-success',
|
||||
fields: [
|
||||
renderAssociationField('关联单据', claimNo || '当前匹配单据'),
|
||||
renderAssociationField('归集结果', `${normalizedUploadedCount} 份成功${normalizedSkippedCount ? `,${normalizedSkippedCount} 份未归集` : ''}`),
|
||||
applicationClaimNo
|
||||
? renderAssociationField('来源申请', applicationClaimNo)
|
||||
: '',
|
||||
renderAssociationField('归集结果', resultText),
|
||||
confidence
|
||||
? renderAssociationField('匹配置信度', confidence)
|
||||
: '',
|
||||
Array.isArray(matchReasons) && matchReasons.length
|
||||
? renderAssociationField('匹配依据', matchReasons.join(';'), { wide: true, muted: true })
|
||||
: '',
|
||||
riskText
|
||||
? renderAssociationField('风险提示', riskText, { wide: true })
|
||||
: '',
|
||||
renderAssociationField('附件', formatAttachmentNames(fileNames), { wide: true })
|
||||
],
|
||||
note: done
|
||||
? '附件已经写入该报销单,可进入详情页继续核对。'
|
||||
: '部分附件没有找到可用明细项,请进入详情页手动核对。'
|
||||
].filter(Boolean),
|
||||
note: requiresReview
|
||||
? '附件已关联,但草稿仍有风险或待确认项,请进入详情页复核后再提交。'
|
||||
: completed
|
||||
? '附件已经写入该报销单,可进入详情页继续核对。'
|
||||
: '没有附件写入,请进入详情页手动核对。'
|
||||
})
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
function formatAssociationIssueList(items = []) {
|
||||
return (Array.isArray(items) ? items : [])
|
||||
.map((item) => String(item?.message || item?.description || item?.detail || item || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(';')
|
||||
}
|
||||
|
||||
function formatServerCandidate(candidate = {}, index = 0) {
|
||||
const claimNo = String(candidate?.claimNo || candidate?.claim_no || '').trim()
|
||||
const applicationClaimNo = String(
|
||||
candidate?.applicationClaimNo || candidate?.application_claim_no || ''
|
||||
).trim()
|
||||
const documentNo = claimNo || applicationClaimNo || '未编号单据'
|
||||
const confidence = String(candidate?.confidence || '').trim()
|
||||
const score = Number(candidate?.score ?? candidate?.confidenceScore ?? candidate?.confidence_score ?? 0) || 0
|
||||
const reasons = Array.isArray(candidate?.matchReasons)
|
||||
? candidate.matchReasons
|
||||
: Array.isArray(candidate?.match_reasons)
|
||||
? candidate.match_reasons
|
||||
: []
|
||||
return [
|
||||
`${index + 1}. ${documentNo}`,
|
||||
claimNo && applicationClaimNo ? `来源申请 ${applicationClaimNo}` : '',
|
||||
confidence ? `置信度 ${confidence}${score ? `(${score})` : ''}` : score ? `匹配分 ${score}` : '',
|
||||
reasons.length ? reasons.join('、') : ''
|
||||
].filter(Boolean).join(',')
|
||||
}
|
||||
|
||||
export function buildAiAttachmentAssociationConfirmationResultMessage({
|
||||
job = null,
|
||||
fileNames = []
|
||||
} = {}) {
|
||||
const candidates = (Array.isArray(job?.candidates) ? job.candidates : [])
|
||||
.slice(0, 3)
|
||||
.map((candidate, index) => formatServerCandidate(candidate, index))
|
||||
const exceptions = formatAssociationIssueList(job?.exceptions)
|
||||
const missingFields = formatAssociationIssueList(job?.missingFields || job?.missing_fields)
|
||||
const riskItems = formatAssociationIssueList(job?.riskItems || job?.risk_items)
|
||||
const matchReasons = (Array.isArray(job?.matchReasons) ? job.matchReasons : job?.match_reasons || [])
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
const confidence = String(job?.confidence || '').trim()
|
||||
const confidenceScore = Number(job?.confidenceScore ?? job?.confidence_score ?? 0) || 0
|
||||
|
||||
return [
|
||||
'我已完成票据匹配分析,但当前信息不足以安全自动归集。',
|
||||
renderAssociationCard({
|
||||
title: '候选单据需要确认',
|
||||
status: '未归集',
|
||||
tone: 'is-warning',
|
||||
ariaLabel: '票据关联候选确认',
|
||||
fields: [
|
||||
renderAssociationField('本次附件', formatAttachmentNames(fileNames)),
|
||||
confidence || confidenceScore
|
||||
? renderAssociationField(
|
||||
'整体置信度',
|
||||
[confidence, confidenceScore ? String(confidenceScore) : ''].filter(Boolean).join(' / ')
|
||||
)
|
||||
: '',
|
||||
matchReasons.length
|
||||
? renderAssociationField('匹配依据', matchReasons.join(';'), { wide: true, muted: true })
|
||||
: '',
|
||||
candidates.length
|
||||
? renderAssociationField('候选单据', candidates.join(';'), { wide: true })
|
||||
: renderAssociationField('候选单据', '暂未形成可安全查看的候选单据', { wide: true }),
|
||||
exceptions
|
||||
? renderAssociationField('待处理异常', exceptions, { wide: true })
|
||||
: '',
|
||||
missingFields
|
||||
? renderAssociationField('待补信息', missingFields, { wide: true })
|
||||
: '',
|
||||
riskItems
|
||||
? renderAssociationField('风险提示', riskItems, { wide: true })
|
||||
: ''
|
||||
].filter(Boolean),
|
||||
note: candidates.length
|
||||
? '我没有写入任何草稿。请先查看候选单据,核对后再决定归集目标。'
|
||||
: '我没有写入任何草稿,避免因信息不足造成误归集。'
|
||||
})
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
162
web/src/utils/attachmentAssociationJobModel.js
Normal file
162
web/src/utils/attachmentAssociationJobModel.js
Normal file
@@ -0,0 +1,162 @@
|
||||
function normalizeText(value = '') {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function normalizeStringList(values = []) {
|
||||
return (Array.isArray(values) ? values : [])
|
||||
.map((item) => normalizeText(item))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeIssue(item = null) {
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
if (typeof item === 'string' || typeof item === 'number') {
|
||||
const message = normalizeText(item)
|
||||
return message ? { code: '', message } : null
|
||||
}
|
||||
if (typeof item !== 'object') {
|
||||
return null
|
||||
}
|
||||
const code = normalizeText(item.code || item.type || item.key)
|
||||
const message = normalizeText(
|
||||
item.message || item.detail || item.description || item.reason || item.label || code
|
||||
)
|
||||
return message ? { code, message } : null
|
||||
}
|
||||
|
||||
function normalizeIssueList(values = []) {
|
||||
return (Array.isArray(values) ? values : [])
|
||||
.map((item) => normalizeIssue(item))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeDraftPayload(payload = null) {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
claimId: normalizeText(payload.claimId || payload.claim_id || payload.id),
|
||||
claimNo: normalizeText(payload.claimNo || payload.claim_no)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAttachmentAssociationCandidate(candidate = null) {
|
||||
if (!candidate || typeof candidate !== 'object') {
|
||||
return null
|
||||
}
|
||||
const claimId = normalizeText(candidate.claimId || candidate.claim_id)
|
||||
const claimNo = normalizeText(candidate.claimNo || candidate.claim_no)
|
||||
const applicationClaimId = normalizeText(candidate.applicationClaimId || candidate.application_claim_id)
|
||||
const applicationClaimNo = normalizeText(candidate.applicationClaimNo || candidate.application_claim_no)
|
||||
if (!claimId && !claimNo && !applicationClaimId && !applicationClaimNo) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
targetType: normalizeText(candidate.targetType || candidate.target_type || 'expense_claim'),
|
||||
expenseCaseId: normalizeText(candidate.expenseCaseId || candidate.expense_case_id),
|
||||
applicationClaimId,
|
||||
applicationClaimNo,
|
||||
claimId,
|
||||
claimNo,
|
||||
confidence: normalizeText(candidate.confidence),
|
||||
score: Number(candidate.score ?? candidate.confidence_score ?? 0) || 0,
|
||||
matchReasons: normalizeStringList(candidate.matchReasons || candidate.match_reasons || [])
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAttachmentAssociationJob(job = null) {
|
||||
if (!job || typeof job !== 'object') {
|
||||
return null
|
||||
}
|
||||
const jobId = normalizeText(job.jobId || job.job_id)
|
||||
if (!jobId) {
|
||||
return null
|
||||
}
|
||||
const resolution = normalizeText(job.resolution)
|
||||
const requiresConfirmation = Boolean(
|
||||
job.requiresConfirmation ?? job.requires_confirmation ?? resolution === 'requires_confirmation'
|
||||
)
|
||||
return {
|
||||
jobId,
|
||||
status: normalizeText(job.status || 'queued') || 'queued',
|
||||
resolution: resolution || (requiresConfirmation ? 'requires_confirmation' : ''),
|
||||
requiresConfirmation,
|
||||
message: normalizeText(job.message),
|
||||
receiptIds: normalizeStringList(job.receiptIds || job.receipt_ids || []),
|
||||
expenseCaseId: normalizeText(job.expenseCaseId || job.expense_case_id),
|
||||
applicationClaimId: normalizeText(job.applicationClaimId || job.application_claim_id),
|
||||
applicationClaimNo: normalizeText(job.applicationClaimNo || job.application_claim_no),
|
||||
claimId: normalizeText(job.claimId || job.claim_id),
|
||||
claimNo: normalizeText(job.claimNo || job.claim_no),
|
||||
confidence: normalizeText(job.confidence),
|
||||
confidenceScore: Number(job.confidenceScore ?? job.confidence_score ?? 0) || 0,
|
||||
matchReasons: normalizeStringList(job.matchReasons || job.match_reasons || []),
|
||||
exceptions: normalizeIssueList(job.exceptions || []),
|
||||
missingFields: normalizeIssueList(job.missingFields || job.missing_fields || []),
|
||||
riskItems: normalizeIssueList(job.riskItems || job.risk_items || []),
|
||||
candidates: (Array.isArray(job.candidates) ? job.candidates : [])
|
||||
.map((candidate) => normalizeAttachmentAssociationCandidate(candidate))
|
||||
.filter(Boolean),
|
||||
draftPayload: normalizeDraftPayload(job.draftPayload || job.draft_payload),
|
||||
uploadedCount: Number(job.uploadedCount ?? job.uploaded_count ?? 0) || 0,
|
||||
skippedCount: Number(job.skippedCount ?? job.skipped_count ?? 0) || 0,
|
||||
error: normalizeText(job.error)
|
||||
}
|
||||
}
|
||||
|
||||
export function isAttachmentAssociationConfirmationResult(job = null) {
|
||||
const normalizedJob = normalizeAttachmentAssociationJob(job)
|
||||
return Boolean(
|
||||
normalizedJob?.requiresConfirmation || normalizedJob?.resolution === 'requires_confirmation'
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveAttachmentAssociationTarget(job = null) {
|
||||
const normalizedJob = normalizeAttachmentAssociationJob(job)
|
||||
if (!normalizedJob) {
|
||||
return { claimId: '', claimNo: '' }
|
||||
}
|
||||
const draftPayload = normalizedJob.draftPayload || {}
|
||||
const firstCandidate = normalizedJob.candidates[0] || {}
|
||||
return {
|
||||
claimId: normalizeText(
|
||||
normalizedJob.claimId || draftPayload.claimId || draftPayload.claim_id || draftPayload.id || firstCandidate.claimId
|
||||
),
|
||||
claimNo: normalizeText(
|
||||
normalizedJob.claimNo || draftPayload.claimNo || draftPayload.claim_no || firstCandidate.claimNo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAttachmentAssociationCandidateActions(job = null) {
|
||||
const normalizedJob = normalizeAttachmentAssociationJob(job)
|
||||
if (!normalizedJob) {
|
||||
return []
|
||||
}
|
||||
return normalizedJob.candidates.slice(0, 3).map((candidate, index) => {
|
||||
const applicationOnly = !candidate.claimId && !candidate.claimNo && Boolean(
|
||||
candidate.applicationClaimId || candidate.applicationClaimNo
|
||||
)
|
||||
const labelPrefix = applicationOnly ? '查看候选申请' : '查看候选草稿'
|
||||
const documentNo = applicationOnly ? candidate.applicationClaimNo : candidate.claimNo
|
||||
return {
|
||||
label: normalizedJob.candidates.length > 1 ? `${labelPrefix} ${index + 1}` : labelPrefix,
|
||||
description: documentNo
|
||||
? `打开候选${applicationOnly ? '申请单' : '报销草稿'} ${documentNo} 核对详情。`
|
||||
: `打开候选${applicationOnly ? '申请单' : '报销草稿'}核对详情。`,
|
||||
icon: 'mdi mdi-open-in-new',
|
||||
action_type: 'open_application_detail',
|
||||
payload: {
|
||||
claim_id: applicationOnly ? candidate.applicationClaimId : candidate.claimId,
|
||||
claim_no: applicationOnly ? candidate.applicationClaimNo : candidate.claimNo,
|
||||
document_type: applicationOnly ? 'application' : 'expense',
|
||||
expense_case_id: candidate.expenseCaseId,
|
||||
application_claim_id: candidate.applicationClaimId,
|
||||
application_claim_no: candidate.applicationClaimNo
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user