2026-06-20 22:04:37 +08:00
|
|
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
const REQUIRED_APPLICATION_EXPENSE_TYPES = new Set(['travel', 'meal'])
|
|
|
|
|
|
|
|
|
|
const APPLICATION_TYPE_ALIASES = {
|
|
|
|
|
travel: new Set(['travel', 'travel_application']),
|
|
|
|
|
meal: new Set([
|
|
|
|
|
'meal',
|
|
|
|
|
'entertainment',
|
|
|
|
|
'meal_application',
|
|
|
|
|
'entertainment_application',
|
|
|
|
|
'business_entertainment_application',
|
|
|
|
|
'hospitality_application'
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GENERIC_APPLICATION_TYPES = new Set(['application', 'expense_application'])
|
2026-06-03 16:28:09 +08:00
|
|
|
const APPROVED_APPLICATION_STATUSES = new Set(['approved', 'completed'])
|
|
|
|
|
const APPROVED_APPLICATION_APPROVAL_KEYS = new Set(['completed'])
|
|
|
|
|
const BLOCKED_APPLICATION_LINK_STATUSES = new Set(['draft', 'returned', 'rejected', 'archived', 'cancelled', 'canceled', 'deleted'])
|
|
|
|
|
const INACTIVE_REIMBURSEMENT_LINK_STATUSES = new Set(['cancelled', 'canceled', 'deleted'])
|
2026-05-27 14:35:17 +08:00
|
|
|
|
|
|
|
|
const STATUS_LABELS = {
|
|
|
|
|
submitted: '审批中',
|
|
|
|
|
approved: '已审批',
|
|
|
|
|
completed: '已完成',
|
|
|
|
|
archived: '已归档',
|
2026-05-28 12:09:49 +08:00
|
|
|
pending_payment: '待付款',
|
|
|
|
|
paid: '已付款'
|
2026-05-27 14:35:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EXPENSE_TYPE_LABELS = {
|
|
|
|
|
travel: '差旅费',
|
|
|
|
|
meal: '业务招待费'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeText(value) {
|
|
|
|
|
return String(value || '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeLower(value) {
|
|
|
|
|
return normalizeText(value).toLowerCase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uniqueValues(values) {
|
|
|
|
|
return Array.from(new Set((Array.isArray(values) ? values : []).map(normalizeText).filter(Boolean)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
function expandIdentityValues(values) {
|
|
|
|
|
const expanded = []
|
|
|
|
|
;(Array.isArray(values) ? values : []).forEach((value) => {
|
|
|
|
|
const normalized = normalizeText(value)
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
expanded.push(normalized)
|
|
|
|
|
const atIndex = normalized.indexOf('@')
|
|
|
|
|
if (atIndex > 0) {
|
|
|
|
|
expanded.push(normalized.slice(0, atIndex))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return uniqueValues(expanded)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
function normalizeClaimNo(claim) {
|
|
|
|
|
return normalizeText(claim?.claim_no || claim?.claimNo).toUpperCase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeExpenseType(claim) {
|
|
|
|
|
return normalizeLower(claim?.expense_type || claim?.expenseType || claim?.type_code || claim?.typeCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeApplicationStatus(claim) {
|
|
|
|
|
return normalizeLower(claim?.status || claim?.state || claim?.approval_status || claim?.approvalStatus)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:28:09 +08:00
|
|
|
function normalizeApprovalKey(claim) {
|
|
|
|
|
return normalizeLower(claim?.approvalKey || claim?.approval_key)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
function normalizeDocumentType(claim) {
|
|
|
|
|
return normalizeLower(
|
|
|
|
|
claim?.document_type_code
|
|
|
|
|
|| claim?.documentTypeCode
|
|
|
|
|
|| claim?.document_type
|
|
|
|
|
|| claim?.documentType
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeApplicationDate(claim) {
|
|
|
|
|
return normalizeText(
|
|
|
|
|
claim?.submitted_at
|
|
|
|
|
|| claim?.submittedAt
|
|
|
|
|
|| claim?.created_at
|
|
|
|
|
|| claim?.createdAt
|
|
|
|
|
|| claim?.occurred_at
|
|
|
|
|
|| claim?.occurredAt
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
function normalizeApplicationDateText(value) {
|
|
|
|
|
const text = normalizeText(value)
|
|
|
|
|
if (!text) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
const matched = text.match(/^(\d{4}-\d{2}-\d{2})/)
|
|
|
|
|
return matched?.[1] || text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeApplicationBusinessTime(claim) {
|
2026-06-02 16:22:59 +08:00
|
|
|
const detail = resolveApplicationDetailPayload(claim)
|
|
|
|
|
const start = normalizeApplicationDateText(
|
|
|
|
|
detail.start_date
|
|
|
|
|
|| detail.startDate
|
|
|
|
|
|| detail.departure_date
|
|
|
|
|
|| detail.departureDate
|
|
|
|
|
|| claim?.start_date
|
|
|
|
|
|| claim?.startDate
|
|
|
|
|
|| claim?.begin_date
|
|
|
|
|
|| claim?.beginDate
|
|
|
|
|
)
|
|
|
|
|
const end = normalizeApplicationDateText(
|
|
|
|
|
detail.end_date
|
|
|
|
|
|| detail.endDate
|
|
|
|
|
|| detail.return_date
|
|
|
|
|
|| detail.returnDate
|
|
|
|
|
|| claim?.end_date
|
|
|
|
|
|| claim?.endDate
|
|
|
|
|
|| claim?.finish_date
|
|
|
|
|
|| claim?.finishDate
|
|
|
|
|
)
|
2026-06-02 14:01:51 +08:00
|
|
|
if (start && end && start !== end) {
|
|
|
|
|
return `${start} 至 ${end}`
|
|
|
|
|
}
|
|
|
|
|
return normalizeApplicationDateText(
|
|
|
|
|
start
|
2026-06-02 16:22:59 +08:00
|
|
|
|| detail.application_business_time
|
|
|
|
|
|| detail.applicationBusinessTime
|
|
|
|
|
|| detail.business_time
|
|
|
|
|
|| detail.businessTime
|
|
|
|
|
|| detail.time_range
|
|
|
|
|
|| detail.timeRange
|
2026-06-02 14:01:51 +08:00
|
|
|
|| claim?.business_time
|
|
|
|
|
|| claim?.businessTime
|
|
|
|
|
|| claim?.time_range
|
|
|
|
|
|| claim?.timeRange
|
|
|
|
|
|| claim?.occurred_at
|
|
|
|
|
|| claim?.occurredAt
|
|
|
|
|
|| claim?.occurred_date
|
|
|
|
|
|| claim?.occurredDate
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
function resolveApplicationDetailPayload(claim) {
|
|
|
|
|
const flags = Array.isArray(claim?.risk_flags_json)
|
|
|
|
|
? claim.risk_flags_json
|
|
|
|
|
: Array.isArray(claim?.riskFlags)
|
|
|
|
|
? claim.riskFlags
|
|
|
|
|
: []
|
|
|
|
|
const detailFlag = flags.find((flag) => (
|
|
|
|
|
flag &&
|
|
|
|
|
typeof flag === 'object' &&
|
|
|
|
|
normalizeLower(flag.source) === 'application_detail'
|
|
|
|
|
))
|
|
|
|
|
const detail = detailFlag?.application_detail || detailFlag?.applicationDetail || {}
|
|
|
|
|
return detail && typeof detail === 'object' ? detail : {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:28:09 +08:00
|
|
|
function resolveRiskFlags(claim) {
|
|
|
|
|
const flags = claim?.risk_flags_json || claim?.riskFlags || []
|
|
|
|
|
return Array.isArray(flags) ? flags : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createReferenceIndex() {
|
|
|
|
|
return {
|
|
|
|
|
ids: new Set(),
|
|
|
|
|
claimNos: new Set()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addApplicationReference(index, idValue, claimNoValue) {
|
|
|
|
|
const id = normalizeText(idValue)
|
|
|
|
|
if (id) {
|
|
|
|
|
index.ids.add(id)
|
|
|
|
|
}
|
|
|
|
|
const claimNo = normalizeText(claimNoValue).toUpperCase()
|
|
|
|
|
if (claimNo) {
|
|
|
|
|
index.claimNos.add(claimNo)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addApplicationReferencesFromPayload(index, payload) {
|
|
|
|
|
if (!payload || typeof payload !== 'object') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addApplicationReference(
|
|
|
|
|
index,
|
|
|
|
|
payload.application_claim_id || payload.applicationClaimId || payload.id,
|
|
|
|
|
payload.application_claim_no || payload.applicationClaimNo || payload.claim_no || payload.claimNo
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectLinkedApplicationReferences(claim) {
|
|
|
|
|
const index = createReferenceIndex()
|
|
|
|
|
addApplicationReferencesFromPayload(index, claim?.relatedApplication)
|
|
|
|
|
addApplicationReferencesFromPayload(index, claim?.related_application)
|
|
|
|
|
resolveRiskFlags(claim).forEach((flag) => {
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
addApplicationReferencesFromPayload(index, flag)
|
|
|
|
|
addApplicationReferencesFromPayload(index, flag.application_detail || flag.applicationDetail)
|
|
|
|
|
addApplicationReferencesFromPayload(index, flag.review_form_values || flag.reviewFormValues)
|
|
|
|
|
addApplicationReferencesFromPayload(index, flag.expense_scene_selection || flag.expenseSceneSelection)
|
|
|
|
|
})
|
|
|
|
|
return index
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasAnyApplicationReference(index) {
|
|
|
|
|
return Boolean(index?.ids?.size || index?.claimNos?.size)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
export function buildLinkedApplicationReferenceIndex(claims) {
|
2026-06-03 16:28:09 +08:00
|
|
|
const index = createReferenceIndex()
|
|
|
|
|
;(Array.isArray(claims) ? claims : []).forEach((claim) => {
|
|
|
|
|
if (isExpenseApplicationClaim(claim)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const status = normalizeApplicationStatus(claim)
|
|
|
|
|
if (INACTIVE_REIMBURSEMENT_LINK_STATUSES.has(status)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const claimReferences = collectLinkedApplicationReferences(claim)
|
|
|
|
|
claimReferences.ids.forEach((id) => index.ids.add(id))
|
|
|
|
|
claimReferences.claimNos.forEach((claimNo) => index.claimNos.add(claimNo))
|
|
|
|
|
})
|
|
|
|
|
return index
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isApplicationAlreadyLinked(claim, linkedApplicationReferences) {
|
|
|
|
|
if (!linkedApplicationReferences) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ownReferences = createReferenceIndex()
|
|
|
|
|
addApplicationReference(
|
|
|
|
|
ownReferences,
|
|
|
|
|
claim?.id || claim?.claim_id || claim?.claimId,
|
|
|
|
|
claim?.claim_no || claim?.claimNo
|
|
|
|
|
)
|
|
|
|
|
if (!hasAnyApplicationReference(ownReferences)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(ownReferences.ids).some((id) => linkedApplicationReferences.ids.has(id))
|
|
|
|
|
|| Array.from(ownReferences.claimNos).some((claimNo) => linkedApplicationReferences.claimNos.has(claimNo))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
function toTimestamp(value) {
|
|
|
|
|
const date = new Date(value)
|
|
|
|
|
return Number.isNaN(date.getTime()) ? 0 : date.getTime()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatAmount(value) {
|
|
|
|
|
const numberValue = Number(String(value ?? '').replace(/[^\d.-]/g, ''))
|
|
|
|
|
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
return `¥${new Intl.NumberFormat('zh-CN', {
|
|
|
|
|
minimumFractionDigits: Number.isInteger(numberValue) ? 0 : 2,
|
|
|
|
|
maximumFractionDigits: 2
|
|
|
|
|
}).format(numberValue)}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function includesAny(text, keywords) {
|
|
|
|
|
const normalized = normalizeText(text)
|
|
|
|
|
return keywords.some((keyword) => normalized.includes(keyword))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildApplicationKeywordText(claim) {
|
|
|
|
|
return [
|
|
|
|
|
claim?.reason,
|
|
|
|
|
claim?.business_reason,
|
|
|
|
|
claim?.title,
|
|
|
|
|
claim?.summary,
|
|
|
|
|
claim?.description,
|
|
|
|
|
claim?.location,
|
|
|
|
|
claim?.business_location,
|
|
|
|
|
claim?.expense_type_label,
|
|
|
|
|
claim?.expenseTypeLabel
|
|
|
|
|
].map(normalizeText).filter(Boolean).join(' ')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchesGenericApplicationByText(claim, expenseType) {
|
|
|
|
|
const haystack = buildApplicationKeywordText(claim)
|
|
|
|
|
if (expenseType === 'travel') {
|
|
|
|
|
return includesAny(haystack, ['差旅', '出差', '住宿', '交通', '行程'])
|
|
|
|
|
}
|
|
|
|
|
if (expenseType === 'meal') {
|
|
|
|
|
return includesAny(haystack, ['招待', '客户', '接待', '宴请', '用餐', '餐饮'])
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function requiresApplicationBeforeReimbursement(expenseType) {
|
|
|
|
|
return REQUIRED_APPLICATION_EXPENSE_TYPES.has(normalizeLower(expenseType))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getRequiredApplicationExpenseLabel(expenseType) {
|
|
|
|
|
return EXPENSE_TYPE_LABELS[normalizeLower(expenseType)] || '报销'
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
export function resolveRequiredApplicationReimbursementType(application = {}) {
|
|
|
|
|
const expenseType = normalizeLower(
|
|
|
|
|
application.application_expense_type
|
|
|
|
|
|| application.expense_type
|
|
|
|
|
|| application.expenseType
|
|
|
|
|
|| application.type_code
|
|
|
|
|
|| application.typeCode
|
|
|
|
|
)
|
|
|
|
|
const source = {
|
|
|
|
|
expense_type: expenseType,
|
|
|
|
|
reason: application.application_reason || application.reason,
|
|
|
|
|
title: application.application_reason || application.reason,
|
|
|
|
|
description: application.application_reason || application.reason,
|
|
|
|
|
location: application.application_location || application.location
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (APPLICATION_TYPE_ALIASES.travel.has(expenseType)) {
|
|
|
|
|
return {
|
|
|
|
|
expenseType: 'travel',
|
|
|
|
|
expenseTypeLabel: EXPENSE_TYPE_LABELS.travel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (APPLICATION_TYPE_ALIASES.meal.has(expenseType)) {
|
|
|
|
|
return {
|
|
|
|
|
expenseType: 'meal',
|
|
|
|
|
expenseTypeLabel: EXPENSE_TYPE_LABELS.meal
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matchesGenericApplicationByText(source, 'meal')) {
|
|
|
|
|
return {
|
|
|
|
|
expenseType: 'meal',
|
|
|
|
|
expenseTypeLabel: EXPENSE_TYPE_LABELS.meal
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matchesGenericApplicationByText(source, 'travel')) {
|
|
|
|
|
return {
|
|
|
|
|
expenseType: 'travel',
|
|
|
|
|
expenseTypeLabel: EXPENSE_TYPE_LABELS.travel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
expenseType: REQUIRED_APPLICATION_EXPENSE_TYPES.has(expenseType) ? expenseType : 'travel',
|
|
|
|
|
expenseTypeLabel: getRequiredApplicationExpenseLabel(
|
|
|
|
|
REQUIRED_APPLICATION_EXPENSE_TYPES.has(expenseType) ? expenseType : 'travel'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
export function isExpenseApplicationClaim(claim) {
|
|
|
|
|
const documentType = normalizeDocumentType(claim)
|
|
|
|
|
const expenseType = normalizeExpenseType(claim)
|
|
|
|
|
const claimNo = normalizeClaimNo(claim)
|
|
|
|
|
|
|
|
|
|
return documentType === 'application'
|
|
|
|
|
|| documentType === 'expense_application'
|
2026-06-20 22:04:37 +08:00
|
|
|
|| isApplicationDocumentNo(claimNo)
|
2026-05-27 14:35:17 +08:00
|
|
|
|| expenseType === 'application'
|
|
|
|
|
|| expenseType.endsWith('_application')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function matchesRequiredApplicationExpenseType(claim, expenseType) {
|
|
|
|
|
const normalizedExpenseType = normalizeLower(expenseType)
|
|
|
|
|
const claimExpenseType = normalizeExpenseType(claim)
|
|
|
|
|
const aliases = APPLICATION_TYPE_ALIASES[normalizedExpenseType] || new Set()
|
|
|
|
|
|
|
|
|
|
if (aliases.has(claimExpenseType)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return GENERIC_APPLICATION_TYPES.has(claimExpenseType)
|
|
|
|
|
&& matchesGenericApplicationByText(claim, normalizedExpenseType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isClaimOwnedByCurrentUser(claim, currentUser = {}) {
|
2026-06-22 15:55:59 +08:00
|
|
|
const userIds = expandIdentityValues([
|
2026-05-27 14:35:17 +08:00
|
|
|
currentUser.id,
|
|
|
|
|
currentUser.employeeId,
|
|
|
|
|
currentUser.employee_id,
|
|
|
|
|
currentUser.employeeNo,
|
|
|
|
|
currentUser.employee_no,
|
|
|
|
|
currentUser.username,
|
|
|
|
|
currentUser.email
|
|
|
|
|
])
|
2026-06-22 15:55:59 +08:00
|
|
|
const claimIds = expandIdentityValues([
|
2026-05-27 14:35:17 +08:00
|
|
|
claim?.employee_id,
|
|
|
|
|
claim?.employeeId,
|
|
|
|
|
claim?.employee_no,
|
|
|
|
|
claim?.employeeNo,
|
2026-06-22 15:55:59 +08:00
|
|
|
claim?.employee_email,
|
|
|
|
|
claim?.employeeEmail,
|
2026-05-27 14:35:17 +08:00
|
|
|
claim?.username,
|
|
|
|
|
claim?.user_id,
|
|
|
|
|
claim?.userId
|
|
|
|
|
])
|
|
|
|
|
if (userIds.length && claimIds.length && claimIds.some((item) => userIds.includes(item))) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userNames = uniqueValues([
|
|
|
|
|
currentUser.name,
|
|
|
|
|
currentUser.user_name,
|
|
|
|
|
currentUser.employeeName,
|
|
|
|
|
currentUser.employee_name,
|
|
|
|
|
currentUser.username
|
|
|
|
|
])
|
|
|
|
|
const claimNames = uniqueValues([
|
|
|
|
|
claim?.employee_name,
|
|
|
|
|
claim?.employeeName,
|
|
|
|
|
claim?.applicant,
|
|
|
|
|
claim?.applicant_name,
|
|
|
|
|
claim?.applicantName
|
|
|
|
|
])
|
|
|
|
|
if (userNames.length && claimNames.length) {
|
|
|
|
|
return claimNames.some((item) => userNames.includes(item))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:28:09 +08:00
|
|
|
export function isUsableRequiredApplicationClaim(claim, linkedApplicationReferences = null) {
|
2026-05-27 14:35:17 +08:00
|
|
|
const status = normalizeApplicationStatus(claim)
|
2026-06-03 16:28:09 +08:00
|
|
|
const approvalKey = normalizeApprovalKey(claim)
|
|
|
|
|
if (BLOCKED_APPLICATION_LINK_STATUSES.has(status)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return (APPROVED_APPLICATION_STATUSES.has(status) || APPROVED_APPLICATION_APPROVAL_KEYS.has(approvalKey))
|
|
|
|
|
&& !isApplicationAlreadyLinked(claim, linkedApplicationReferences)
|
2026-05-27 14:35:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function normalizeRequiredApplicationCandidate(claim) {
|
2026-06-02 16:22:59 +08:00
|
|
|
const detail = resolveApplicationDetailPayload(claim)
|
2026-05-27 14:35:17 +08:00
|
|
|
const claimNo = normalizeText(claim?.claim_no || claim?.claimNo)
|
2026-06-02 16:22:59 +08:00
|
|
|
const location = normalizeText(
|
|
|
|
|
detail.location
|
|
|
|
|
|| detail.application_location
|
|
|
|
|
|| claim?.location
|
|
|
|
|
|| claim?.business_location
|
|
|
|
|
|| claim?.businessLocation
|
|
|
|
|
)
|
|
|
|
|
const amount = normalizeText(
|
|
|
|
|
detail.amount
|
|
|
|
|
|| detail.application_amount
|
|
|
|
|
|| claim?.amount
|
|
|
|
|
|| claim?.budget_amount
|
|
|
|
|
|| claim?.budgetAmount
|
|
|
|
|
)
|
|
|
|
|
const amountText = formatAmount(amount)
|
2026-05-27 14:35:17 +08:00
|
|
|
const status = normalizeApplicationStatus(claim)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: normalizeText(claim?.id || claim?.claim_id || claim?.claimId),
|
|
|
|
|
claim_no: claimNo,
|
|
|
|
|
expense_type: normalizeExpenseType(claim),
|
2026-06-02 16:22:59 +08:00
|
|
|
reason: normalizeText(detail.reason || detail.application_reason || claim?.reason || claim?.business_reason || claim?.description || claim?.title),
|
2026-05-27 14:35:17 +08:00
|
|
|
location,
|
2026-06-02 16:22:59 +08:00
|
|
|
amount,
|
2026-05-27 14:35:17 +08:00
|
|
|
amount_label: amountText,
|
2026-06-02 16:22:59 +08:00
|
|
|
business_time: normalizeText(
|
|
|
|
|
detail.application_business_time
|
|
|
|
|
|| detail.applicationBusinessTime
|
|
|
|
|
|| detail.business_time
|
|
|
|
|
|| detail.businessTime
|
|
|
|
|
|| detail.time_range
|
|
|
|
|
|| detail.timeRange
|
|
|
|
|
|| detail.time
|
|
|
|
|
|| detail.application_time
|
|
|
|
|
) || normalizeApplicationBusinessTime(claim),
|
|
|
|
|
days: normalizeText(detail.days || detail.application_days),
|
|
|
|
|
transport_mode: normalizeText(detail.transport_mode || detail.application_transport_mode),
|
|
|
|
|
lodging_daily_cap: normalizeText(detail.lodging_daily_cap || detail.application_lodging_daily_cap),
|
|
|
|
|
subsidy_daily_cap: normalizeText(detail.subsidy_daily_cap || detail.application_subsidy_daily_cap),
|
|
|
|
|
transport_policy: normalizeText(detail.transport_policy || detail.application_transport_policy),
|
|
|
|
|
policy_estimate: normalizeText(detail.policy_estimate || detail.application_policy_estimate),
|
|
|
|
|
rule_name: normalizeText(detail.rule_name || detail.application_rule_name),
|
|
|
|
|
rule_version: normalizeText(detail.rule_version || detail.application_rule_version),
|
2026-05-27 14:35:17 +08:00
|
|
|
status,
|
|
|
|
|
status_label: STATUS_LABELS[status] || normalizeText(claim?.approval_stage || claim?.approvalStage || status),
|
|
|
|
|
application_date: normalizeApplicationDate(claim)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function filterRequiredApplicationCandidates(claimsPayload, expenseType, currentUser = {}) {
|
|
|
|
|
const claims = Array.isArray(claimsPayload)
|
|
|
|
|
? claimsPayload
|
|
|
|
|
: Array.isArray(claimsPayload?.items)
|
|
|
|
|
? claimsPayload.items
|
|
|
|
|
: Array.isArray(claimsPayload?.claims)
|
|
|
|
|
? claimsPayload.claims
|
|
|
|
|
: []
|
|
|
|
|
|
2026-06-03 16:28:09 +08:00
|
|
|
const linkedApplicationReferences = buildLinkedApplicationReferenceIndex(claims)
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
return claims
|
|
|
|
|
.filter((claim) => (
|
|
|
|
|
isExpenseApplicationClaim(claim)
|
2026-06-03 16:28:09 +08:00
|
|
|
&& isUsableRequiredApplicationClaim(claim, linkedApplicationReferences)
|
2026-05-27 14:35:17 +08:00
|
|
|
&& isClaimOwnedByCurrentUser(claim, currentUser)
|
|
|
|
|
&& matchesRequiredApplicationExpenseType(claim, expenseType)
|
|
|
|
|
))
|
|
|
|
|
.map(normalizeRequiredApplicationCandidate)
|
|
|
|
|
.sort((left, right) => toTimestamp(right.application_date) - toTimestamp(left.application_date))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildRequiredApplicationActions(applications, actionType) {
|
|
|
|
|
return (Array.isArray(applications) ? applications : []).map((application) => {
|
|
|
|
|
const claimNo = normalizeText(application.claim_no) || '未编号申请单'
|
|
|
|
|
const description = [
|
|
|
|
|
application.status_label,
|
2026-06-02 14:01:51 +08:00
|
|
|
application.business_time && `时间:${application.business_time}`,
|
2026-05-27 14:35:17 +08:00
|
|
|
application.location && `地点:${application.location}`,
|
|
|
|
|
application.amount_label && `预算:${application.amount_label}`,
|
|
|
|
|
application.reason && `事由:${application.reason}`
|
|
|
|
|
].filter(Boolean).join(' · ')
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
label: claimNo,
|
|
|
|
|
description,
|
|
|
|
|
icon: 'mdi mdi-file-link-outline',
|
|
|
|
|
action_type: actionType,
|
|
|
|
|
payload: {
|
|
|
|
|
application_claim_id: application.id,
|
|
|
|
|
application_claim_no: application.claim_no,
|
|
|
|
|
application_expense_type: application.expense_type,
|
|
|
|
|
application_reason: application.reason,
|
|
|
|
|
application_location: application.location,
|
|
|
|
|
application_amount: application.amount,
|
|
|
|
|
application_amount_label: application.amount_label,
|
2026-06-02 14:01:51 +08:00
|
|
|
application_business_time: application.business_time,
|
2026-06-02 16:22:59 +08:00
|
|
|
application_days: application.days,
|
|
|
|
|
application_transport_mode: application.transport_mode,
|
|
|
|
|
application_lodging_daily_cap: application.lodging_daily_cap,
|
|
|
|
|
application_subsidy_daily_cap: application.subsidy_daily_cap,
|
|
|
|
|
application_transport_policy: application.transport_policy,
|
|
|
|
|
application_policy_estimate: application.policy_estimate,
|
|
|
|
|
application_rule_name: application.rule_name,
|
|
|
|
|
application_rule_version: application.rule_version,
|
2026-05-27 14:35:17 +08:00
|
|
|
application_status: application.status,
|
|
|
|
|
application_status_label: application.status_label,
|
|
|
|
|
application_date: application.application_date
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildRequiredApplicationSelectionText(expenseType, applications) {
|
|
|
|
|
const label = getRequiredApplicationExpenseLabel(expenseType)
|
|
|
|
|
return [
|
|
|
|
|
`发起“${label}”报销前,需要先关联对应的申请单。`,
|
|
|
|
|
'',
|
|
|
|
|
`我查到 ${applications.length} 个可关联申请单,请先选择其中一个。`,
|
2026-06-24 10:42:50 +08:00
|
|
|
'选择后,我会继续向您收集本次报销依据。'
|
2026-05-27 14:35:17 +08:00
|
|
|
].join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildRequiredApplicationMissingText(expenseType) {
|
|
|
|
|
const label = getRequiredApplicationExpenseLabel(expenseType)
|
|
|
|
|
return [
|
|
|
|
|
`发起“${label}”报销前,需要先关联对应的申请单。`,
|
|
|
|
|
'',
|
2026-06-24 10:42:50 +08:00
|
|
|
`我没有查到您名下可关联的“${label}”申请单,所以当前不能继续这类报销流程。`,
|
2026-05-27 14:35:17 +08:00
|
|
|
'请先切换到申请助手发起相关申请;申请单存在后,再回到报销助手继续。'
|
|
|
|
|
].join('\n')
|
|
|
|
|
}
|