feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
import { computed, reactive, ref } from 'vue'
|
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
|
import { fetchAllExpenseClaims } from '../services/reimbursements.js'
|
2026-06-20 22:04:37 +08:00
|
|
|
|
import { isApplicationDocumentNo } from '../utils/documentClassification.js'
|
2026-06-03 15:46:56 +08:00
|
|
|
|
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../utils/riskFlags.js'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
|
|
|
|
|
const EXPENSE_TYPE_LABELS = {
|
|
|
|
|
|
travel: '差旅费',
|
2026-05-25 13:35:39 +08:00
|
|
|
|
travel_application: '差旅费用申请',
|
|
|
|
|
|
expense_application: '费用申请',
|
|
|
|
|
|
purchase_application: '采购费用申请',
|
|
|
|
|
|
meeting_application: '会务费用申请',
|
2026-05-21 10:57:06 +08:00
|
|
|
|
train_ticket: '火车票',
|
|
|
|
|
|
flight_ticket: '机票',
|
2026-05-22 16:00:19 +08:00
|
|
|
|
ship_ticket: '轮船票',
|
|
|
|
|
|
ferry_ticket: '轮船票',
|
2026-05-21 10:57:06 +08:00
|
|
|
|
hotel_ticket: '住宿票',
|
|
|
|
|
|
ride_ticket: '乘车',
|
|
|
|
|
|
travel_allowance: '出差补贴',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
entertainment: '业务招待费',
|
2026-05-26 12:16:20 +08:00
|
|
|
|
marketing: '市场推广费',
|
2026-05-22 23:47:28 +08:00
|
|
|
|
office: '办公用品费',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
meeting: '会务费',
|
|
|
|
|
|
training: '培训费',
|
2026-05-26 12:16:20 +08:00
|
|
|
|
software: '软件服务费',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
hotel: '住宿费',
|
|
|
|
|
|
transport: '交通费',
|
2026-05-22 23:47:28 +08:00
|
|
|
|
meal: '业务招待费',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
other: '其他费用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:48:27 +00:00
|
|
|
|
const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([
|
|
|
|
|
|
'travel',
|
2026-05-21 10:57:06 +08:00
|
|
|
|
'train_ticket',
|
|
|
|
|
|
'flight_ticket',
|
|
|
|
|
|
'hotel_ticket',
|
|
|
|
|
|
'ride_ticket',
|
2026-05-13 06:48:27 +00:00
|
|
|
|
'meeting',
|
|
|
|
|
|
'entertainment'
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-21 10:57:06 +08:00
|
|
|
|
const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
|
2026-06-03 17:31:40 +08:00
|
|
|
|
const STANDARD_ADJUSTMENT_RISK_SOURCE = 'reimbursement_standard_adjustment'
|
2026-05-21 10:57:06 +08:00
|
|
|
|
const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket'])
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'])
|
|
|
|
|
|
const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket'])
|
2026-06-02 16:22:59 +08:00
|
|
|
|
const DOCUMENT_BACKED_EXPENSE_TYPES = new Set([
|
|
|
|
|
|
'train_ticket',
|
|
|
|
|
|
'flight_ticket',
|
|
|
|
|
|
'ship_ticket',
|
|
|
|
|
|
'ferry_ticket',
|
|
|
|
|
|
'hotel_ticket',
|
|
|
|
|
|
'ride_ticket'
|
|
|
|
|
|
])
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const DOCUMENT_TYPE_APPLICATION = 'application'
|
|
|
|
|
|
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const RELATED_APPLICATION_STEP_LABEL = '关联单据'
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const APPLICATION_LINK_STATUS_STEP_LABEL = '关联单据状态'
|
|
|
|
|
|
const APPLICATION_ARCHIVE_STAGE_LABEL = '申请归档'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const ARCHIVED_STEP_LABEL = '已归档'
|
2026-05-21 10:57:06 +08:00
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const REIMBURSEMENT_PROGRESS_LABELS = [
|
2026-05-30 15:46:51 +08:00
|
|
|
|
RELATED_APPLICATION_STEP_LABEL,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
'待提交',
|
|
|
|
|
|
'直属领导审批',
|
|
|
|
|
|
'财务审批',
|
2026-05-28 12:09:49 +08:00
|
|
|
|
'待付款',
|
2026-05-30 15:46:51 +08:00
|
|
|
|
'已付款',
|
|
|
|
|
|
ARCHIVED_STEP_LABEL
|
2026-05-13 03:29:10 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const APPLICATION_PROGRESS_LABELS = [
|
|
|
|
|
|
'创建申请',
|
|
|
|
|
|
'直属领导审批',
|
2026-05-27 17:31:27 +08:00
|
|
|
|
'预算管理者审批',
|
2026-06-06 17:19:07 +08:00
|
|
|
|
APPLICATION_LINK_STATUS_STEP_LABEL,
|
|
|
|
|
|
ARCHIVED_STEP_LABEL
|
2026-05-25 13:35:39 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
|
|
|
|
|
|
'创建申请',
|
|
|
|
|
|
'直属领导审批',
|
2026-06-06 17:19:07 +08:00
|
|
|
|
APPLICATION_LINK_STATUS_STEP_LABEL,
|
|
|
|
|
|
ARCHIVED_STEP_LABEL
|
2026-06-01 17:07:14 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
function parseNumber(value) {
|
|
|
|
|
|
const nextValue = Number(value)
|
|
|
|
|
|
return Number.isFinite(nextValue) ? nextValue : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 17:31:40 +08:00
|
|
|
|
function parseOptionalAmount(value) {
|
|
|
|
|
|
if (value === null || value === undefined || String(value).trim() === '') {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const amount = Number(value)
|
|
|
|
|
|
return Number.isFinite(amount) && amount >= 0 ? amount : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStandardAdjustmentMapFromClaim(claim = {}) {
|
|
|
|
|
|
const flags = Array.isArray(claim?.risk_flags_json)
|
|
|
|
|
|
? claim.risk_flags_json
|
|
|
|
|
|
: Array.isArray(claim?.riskFlags)
|
|
|
|
|
|
? claim.riskFlags
|
|
|
|
|
|
: []
|
|
|
|
|
|
const adjustmentMap = new Map()
|
|
|
|
|
|
|
|
|
|
|
|
flags.forEach((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (String(flag.source || '').trim() !== STANDARD_ADJUSTMENT_RISK_SOURCE) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const itemId = String(flag.item_id || flag.itemId || '').trim()
|
|
|
|
|
|
const reimbursableAmount = parseOptionalAmount(flag.reimbursable_amount ?? flag.reimbursableAmount)
|
|
|
|
|
|
if (!itemId || reimbursableAmount === null) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
adjustmentMap.set(itemId, {
|
|
|
|
|
|
originalAmount: parseOptionalAmount(flag.original_amount ?? flag.originalAmount),
|
|
|
|
|
|
reimbursableAmount,
|
|
|
|
|
|
employeeAbsorbedAmount: parseOptionalAmount(flag.employee_absorbed_amount ?? flag.employeeAbsorbedAmount) || 0,
|
|
|
|
|
|
message: String(flag.message || flag.summary || '').trim()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return adjustmentMap
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
function toDate(value) {
|
|
|
|
|
|
if (!value) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextDate = new Date(value)
|
|
|
|
|
|
return Number.isNaN(nextDate.getTime()) ? null : nextDate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(value) {
|
|
|
|
|
|
const nextDate = toDate(value)
|
|
|
|
|
|
if (!nextDate) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const year = nextDate.getFullYear()
|
|
|
|
|
|
const month = String(nextDate.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
|
const day = String(nextDate.getDate()).padStart(2, '0')
|
|
|
|
|
|
return `${year}-${month}-${day}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value) {
|
|
|
|
|
|
const nextDate = toDate(value)
|
|
|
|
|
|
if (!nextDate) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hours = String(nextDate.getHours()).padStart(2, '0')
|
|
|
|
|
|
const minutes = String(nextDate.getMinutes()).padStart(2, '0')
|
|
|
|
|
|
return `${formatDate(nextDate)} ${hours}:${minutes}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
function formatDurationFrom(value, now = Date.now()) {
|
|
|
|
|
|
const startAt = toDate(value)
|
|
|
|
|
|
if (!startAt) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const diffMs = Math.max(0, Number(now) - startAt.getTime())
|
|
|
|
|
|
const totalMinutes = Math.floor(diffMs / (60 * 1000))
|
|
|
|
|
|
if (totalMinutes < 1) {
|
|
|
|
|
|
return '刚刚'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const days = Math.floor(totalMinutes / (24 * 60))
|
|
|
|
|
|
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
|
|
|
|
|
|
const minutes = totalMinutes % 60
|
|
|
|
|
|
|
|
|
|
|
|
if (days > 0) {
|
|
|
|
|
|
return hours > 0 ? `${days}天${hours}小时` : `${days}天`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
|
return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${minutes}分钟`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
function formatAmount(value) {
|
|
|
|
|
|
return new Intl.NumberFormat('zh-CN', {
|
|
|
|
|
|
style: 'currency',
|
|
|
|
|
|
currency: 'CNY',
|
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
|
maximumFractionDigits: Number.isInteger(value) ? 0 : 2
|
|
|
|
|
|
}).format(parseNumber(value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTypeLabel(typeCode) {
|
|
|
|
|
|
return EXPENSE_TYPE_LABELS[String(typeCode || '').trim()] || EXPENSE_TYPE_LABELS.other
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
function resolveDocumentTypeMeta(claim, typeCode) {
|
|
|
|
|
|
const explicitType = String(
|
|
|
|
|
|
claim?.document_type_code
|
|
|
|
|
|
|| claim?.documentTypeCode
|
|
|
|
|
|
|| claim?.document_type
|
|
|
|
|
|
|| claim?.documentType
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
const claimNo = String(claim?.claim_no || claim?.claimNo || '').trim().toUpperCase()
|
|
|
|
|
|
const normalizedType = String(typeCode || '').trim()
|
|
|
|
|
|
const isApplication =
|
|
|
|
|
|
explicitType === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
|| explicitType === 'expense_application'
|
2026-06-20 22:04:37 +08:00
|
|
|
|
|| isApplicationDocumentNo(claimNo)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
|| normalizedType === 'application'
|
|
|
|
|
|
|| normalizedType.endsWith('_application')
|
|
|
|
|
|
|
|
|
|
|
|
return isApplication
|
|
|
|
|
|
? { documentTypeCode: DOCUMENT_TYPE_APPLICATION, documentTypeLabel: '申请单' }
|
|
|
|
|
|
: { documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT, documentTypeLabel: '报销单' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:48:27 +00:00
|
|
|
|
function normalizeExpenseType(typeCode) {
|
|
|
|
|
|
return String(typeCode || '').trim() || 'other'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isLocationRequiredExpenseType(typeCode) {
|
|
|
|
|
|
return LOCATION_REQUIRED_EXPENSE_TYPES.has(normalizeExpenseType(typeCode))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveLocationDisplay(location, typeCode) {
|
|
|
|
|
|
const normalized = String(location || '').trim()
|
|
|
|
|
|
if (normalized) {
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return isLocationRequiredExpenseType(typeCode) ? '待补充' : '非必填'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function resolveExpenseDescriptionDetail(itemType, itemLocation) {
|
|
|
|
|
|
const normalizedType = normalizeExpenseType(itemType)
|
|
|
|
|
|
if (ROUTE_DESCRIPTION_EXPENSE_TYPES.has(normalizedType)) {
|
|
|
|
|
|
return '起始地-目的地'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (HOTEL_DESCRIPTION_EXPENSE_TYPES.has(normalizedType)) {
|
|
|
|
|
|
return '目的地酒店'
|
|
|
|
|
|
}
|
|
|
|
|
|
return resolveLocationDisplay(itemLocation, normalizedType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 10:57:06 +08:00
|
|
|
|
function resolveExpenseItemViewId(item, index, claim) {
|
|
|
|
|
|
return String(item?.id || `${claim?.id || 'claim'}-item-${index}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildTravelTimeLabelMap(items, claim) {
|
|
|
|
|
|
const travelItems = items
|
|
|
|
|
|
.map((item, index) => {
|
|
|
|
|
|
const itemType = normalizeExpenseType(item?.item_type || claim?.expense_type)
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: resolveExpenseItemViewId(item, index, claim),
|
|
|
|
|
|
index,
|
|
|
|
|
|
itemType,
|
|
|
|
|
|
itemDate: formatDate(item?.item_date),
|
|
|
|
|
|
isSystemGenerated: Boolean(item?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType))
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter((item) => !item.isSystemGenerated && LONG_DISTANCE_TRAVEL_EXPENSE_TYPES.has(item.itemType))
|
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
|
const dateCompare = String(left.itemDate || '').localeCompare(String(right.itemDate || ''))
|
|
|
|
|
|
return dateCompare || left.index - right.index
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const labels = new Map()
|
|
|
|
|
|
travelItems.forEach((item, index) => {
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
|
labels.set(item.id, '出发时间')
|
|
|
|
|
|
} else if (index === travelItems.length - 1) {
|
|
|
|
|
|
labels.set(item.id, '返回时间')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
labels.set(item.id, '中转时间')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return labels
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, claim, travelTimeLabelMap }) {
|
|
|
|
|
|
if (isSystemGenerated) {
|
|
|
|
|
|
return '系统自动计算'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (travelTimeLabelMap?.has(id)) {
|
|
|
|
|
|
return travelTimeLabelMap.get(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (itemType === 'ride_ticket') {
|
|
|
|
|
|
return '乘车时间'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (itemType === 'hotel_ticket') {
|
|
|
|
|
|
return '住宿时间'
|
|
|
|
|
|
}
|
|
|
|
|
|
return claim?.expense_type === 'travel' ? '出行时间' : '业务发生时间'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:48:27 +00:00
|
|
|
|
function resolveAttachmentDisplayName(value) {
|
|
|
|
|
|
const normalized = String(value || '').trim()
|
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return normalized.split('/').filter(Boolean).pop() || normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
|
function hasRelatedApplicationContext(claim) {
|
|
|
|
|
|
return Boolean(findRelatedApplicationEvent(claim))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isDocumentBackedRawExpenseItem(item) {
|
|
|
|
|
|
const invoiceId = normalizeText(item?.invoice_id || item?.invoiceId)
|
|
|
|
|
|
if (invoiceId) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DOCUMENT_BACKED_EXPENSE_TYPES.has(normalizeExpenseType(item?.item_type || item?.itemType))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractTravelDayCount(value) {
|
|
|
|
|
|
const matched = normalizeText(value).replace(/\s+/g, '').match(/(\d{1,2})天/)
|
|
|
|
|
|
return matched ? parseNumber(matched[1]) : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isStaleApplicationAllowanceRawItem(item, claim) {
|
|
|
|
|
|
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
|
|
|
|
|
if (itemType !== 'travel_allowance') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const related = resolveRelatedApplicationInfo(claim)
|
|
|
|
|
|
const applicationDays = extractTravelDayCount(related?.days)
|
|
|
|
|
|
const itemDays = extractTravelDayCount(item?.item_reason || item?.itemReason)
|
|
|
|
|
|
return applicationDays > 0 && itemDays > 0 && applicationDays !== itemDays
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isApplicationLinkPlaceholderRawItem(item, claim) {
|
|
|
|
|
|
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
|
|
|
|
|
if (SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const claimType = normalizeExpenseType(claim?.expense_type || claim?.expenseType)
|
|
|
|
|
|
if (itemType && claimType && itemType !== claimType) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const reason = normalizeText(item?.item_reason || item?.itemReason)
|
|
|
|
|
|
if (!reason || reason === '待补充') {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const related = resolveRelatedApplicationInfo(claim)
|
|
|
|
|
|
const linkedReasons = new Set([
|
|
|
|
|
|
normalizeText(claim?.reason),
|
|
|
|
|
|
normalizeText(related?.reason)
|
|
|
|
|
|
].filter(Boolean))
|
|
|
|
|
|
return linkedReasons.has(reason)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function filterVisibleExpenseRawItems(items, claim) {
|
|
|
|
|
|
const rawItems = Array.isArray(items) ? items : []
|
|
|
|
|
|
if (!rawItems.length || !hasRelatedApplicationContext(claim)) {
|
|
|
|
|
|
return rawItems
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hasRealExpenseItem = rawItems.some((item) => (
|
|
|
|
|
|
isDocumentBackedRawExpenseItem(item)
|
|
|
|
|
|
&& !SYSTEM_GENERATED_EXPENSE_TYPES.has(normalizeExpenseType(item?.item_type || item?.itemType))
|
|
|
|
|
|
))
|
|
|
|
|
|
if (!hasRealExpenseItem) {
|
|
|
|
|
|
return rawItems.filter((item) => !isApplicationLinkPlaceholderRawItem(item, claim))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return rawItems.filter((item) => {
|
|
|
|
|
|
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
|
|
|
|
|
if (SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) {
|
|
|
|
|
|
return !isStaleApplicationAllowanceRawItem(item, claim)
|
|
|
|
|
|
}
|
|
|
|
|
|
return !isApplicationLinkPlaceholderRawItem(item, claim)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
function resolveApprovalMeta(status) {
|
|
|
|
|
|
const normalized = String(status || '').trim().toLowerCase()
|
|
|
|
|
|
|
|
|
|
|
|
if (normalized === 'draft') {
|
|
|
|
|
|
return { key: 'draft', label: '草稿', tone: 'draft' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 14:32:35 +08:00
|
|
|
|
if (normalized === 'returned') {
|
|
|
|
|
|
return { key: 'supplement', label: '待提交', tone: 'warning' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (normalized === 'supplement') {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return { key: 'supplement', label: '待补充', tone: 'warning' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (normalized === 'pending_payment') {
|
|
|
|
|
|
return { key: 'pending_payment', label: '待付款', tone: 'warning' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (normalized === 'paid') {
|
|
|
|
|
|
return { key: 'completed', label: '已付款', tone: 'success' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (['approved', 'completed', 'paid'].includes(normalized)) {
|
|
|
|
|
|
return { key: 'completed', label: '已完成', tone: 'success' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (['rejected', 'cancelled'].includes(normalized)) {
|
|
|
|
|
|
return { key: 'rejected', label: '已退回', tone: 'danger' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { key: 'in_progress', label: '审批中', tone: 'info' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
function resolveWorkflowNode(claim, approvalMeta, isApplicationDocument = false) {
|
2026-05-20 14:32:35 +08:00
|
|
|
|
if (String(claim?.status || '').trim().toLowerCase() === 'returned') {
|
|
|
|
|
|
return '待提交'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const rawNode = String(claim?.approval_stage || '').trim()
|
|
|
|
|
|
|
|
|
|
|
|
if (rawNode) {
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (
|
|
|
|
|
|
isApplicationDocument
|
|
|
|
|
|
&& approvalMeta.key === 'completed'
|
|
|
|
|
|
&& (
|
|
|
|
|
|
rawNode === '审批完成'
|
|
|
|
|
|
|| rawNode.includes('审批完成')
|
|
|
|
|
|
|| rawNode.includes('申请完成')
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return APPLICATION_LINK_STATUS_STEP_LABEL
|
|
|
|
|
|
}
|
2026-06-02 14:01:51 +08:00
|
|
|
|
if (rawNode === '审批流转' || rawNode.includes('AI预审') || rawNode.includes('AI验审')) {
|
|
|
|
|
|
return approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' ? '待提交' : '直属领导审批'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (rawNode === '待补充') {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return '待提交'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
return rawNode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
|
|
|
|
|
|
return '待提交'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (approvalMeta.key === 'pending_payment') {
|
|
|
|
|
|
return '待付款'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (approvalMeta.key === 'completed') {
|
2026-05-28 12:09:49 +08:00
|
|
|
|
const normalizedStatus = String(claim?.status || '').trim().toLowerCase()
|
2026-06-06 17:19:07 +08:00
|
|
|
|
return isApplicationDocument ? APPLICATION_LINK_STATUS_STEP_LABEL : normalizedStatus === 'paid' ? '已付款' : '归档入账'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return '直属领导审批'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stringifyRiskFlag(value) {
|
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
|
return value.trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!value || typeof value !== 'object') {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const key of ['message', 'label', 'reason', 'name']) {
|
|
|
|
|
|
const nextValue = String(value[key] || '').trim()
|
|
|
|
|
|
if (nextValue) {
|
|
|
|
|
|
return nextValue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const RISK_TONE_LABELS = {
|
|
|
|
|
|
high: '高风险',
|
|
|
|
|
|
medium: '中风险',
|
|
|
|
|
|
low: '低风险'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveHighestRiskTone(flags) {
|
|
|
|
|
|
const tones = flags.map((item) => normalizeRiskFlagTone(item)).filter(Boolean)
|
|
|
|
|
|
if (tones.includes('high')) {
|
|
|
|
|
|
return 'high'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tones.includes('medium')) {
|
|
|
|
|
|
return 'medium'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tones.includes('low')) {
|
|
|
|
|
|
return 'low'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'low'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRiskMeta(riskFlags) {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (!Array.isArray(riskFlags) || !riskFlags.length) {
|
2026-06-03 15:46:56 +08:00
|
|
|
|
return { summary: '无', tone: 'low', label: '无' }
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const actionableFlags = filterActionableRiskFlags(riskFlags)
|
|
|
|
|
|
const items = actionableFlags.map((item) => stringifyRiskFlag(item)).filter(Boolean)
|
|
|
|
|
|
if (!items.length) {
|
|
|
|
|
|
return { summary: '无', tone: 'low', label: '无' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tone = resolveHighestRiskTone(actionableFlags)
|
|
|
|
|
|
return {
|
|
|
|
|
|
summary: items.join(';'),
|
|
|
|
|
|
tone,
|
|
|
|
|
|
label: RISK_TONE_LABELS[tone] || '待关注'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildRiskSummary(riskFlags) {
|
|
|
|
|
|
return buildRiskMeta(riskFlags).summary
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildOccurredDisplay(claim) {
|
|
|
|
|
|
const itemDates = Array.isArray(claim?.items)
|
|
|
|
|
|
? claim.items.map((item) => formatDate(item?.item_date)).filter(Boolean)
|
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
|
|
if (!itemDates.length) {
|
|
|
|
|
|
return formatDate(claim?.occurred_at) || '待补充'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sortedDates = [...new Set(itemDates)].sort()
|
|
|
|
|
|
if (sortedDates.length === 1) {
|
|
|
|
|
|
return sortedDates[0]
|
2026-04-29 23:35:56 +08:00
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
|
|
|
|
|
return `${sortedDates[0]} ~ ${sortedDates[sortedDates.length - 1]}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveProgressCurrentIndex(approvalMeta, workflowNode) {
|
|
|
|
|
|
const normalizedNode = String(workflowNode || '').trim()
|
|
|
|
|
|
|
|
|
|
|
|
if (approvalMeta.key === 'completed') {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 6
|
2026-05-28 12:09:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (approvalMeta.key === 'pending_payment') {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 4
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (normalizedNode.includes('已付款')) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 5
|
2026-05-28 12:09:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (normalizedNode.includes('待付款')) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 4
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (normalizedNode.includes('归档') || normalizedNode.includes('入账')) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 6
|
2026-05-28 12:09:49 +08:00
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (normalizedNode.includes('财务')) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 3
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
normalizedNode.includes('直属领导')
|
|
|
|
|
|
|| normalizedNode.includes('领导审批')
|
|
|
|
|
|
|| normalizedNode.includes('部门负责人')
|
|
|
|
|
|
|| normalizedNode.includes('负责人审批')
|
|
|
|
|
|
) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return 2
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
2026-05-20 09:36:01 +08:00
|
|
|
|
if (normalizedNode.includes('AI预审') || normalizedNode.includes('AI验审') || normalizedNode.includes('审批流转')) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
return approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' ? 1 : 2
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (normalizedNode.includes('待提交')) {
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
|
|
|
|
|
|
const normalizedNode = String(workflowNode || '').trim()
|
|
|
|
|
|
|
|
|
|
|
|
if (approvalMeta.key === 'completed') {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 3 : 2
|
2026-05-25 13:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return 3
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (normalizedNode.includes(APPLICATION_LINK_STATUS_STEP_LABEL)) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return 2
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
2026-05-25 13:35:39 +08:00
|
|
|
|
if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return 2
|
2026-05-27 17:31:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (normalizedNode.includes('预算')) {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
return 2
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
normalizedNode.includes('直属领导')
|
|
|
|
|
|
|| normalizedNode.includes('领导审批')
|
|
|
|
|
|
|| normalizedNode.includes('部门负责人')
|
|
|
|
|
|
|| normalizedNode.includes('负责人审批')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
function isApplicationArchivedWorkflow(claim, workflowNode) {
|
|
|
|
|
|
const normalizedNode = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
|
|
|
|
|
|
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return getRiskFlags(claim).some((flag) => (
|
|
|
|
|
|
flag
|
|
|
|
|
|
&& typeof flag === 'object'
|
|
|
|
|
|
&& normalizeText(flag.source) === 'application_archive_sync'
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveApplicationLinkedReimbursementNo(claim) {
|
|
|
|
|
|
for (const flag of [...getRiskFlags(claim)].reverse()) {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const generatedNo = normalizeText(
|
|
|
|
|
|
flag.generated_draft_claim_no
|
|
|
|
|
|
|| flag.generatedDraftClaimNo
|
|
|
|
|
|
|| flag.reimbursement_claim_no
|
|
|
|
|
|
|| flag.reimbursementClaimNo
|
|
|
|
|
|
)
|
|
|
|
|
|
if (generatedNo) {
|
|
|
|
|
|
return generatedNo
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildApplicationLinkStatusStepMeta(claim) {
|
|
|
|
|
|
const reimbursementNo = resolveApplicationLinkedReimbursementNo(claim)
|
|
|
|
|
|
const updatedAt = formatDateTime(claim?.updated_at)
|
|
|
|
|
|
return reimbursementNo
|
|
|
|
|
|
? buildProgressStepMeta(`关联中 ${reimbursementNo}`, updatedAt)
|
|
|
|
|
|
: buildProgressStepMeta('未关联', updatedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
function normalizeText(value) {
|
|
|
|
|
|
return String(value || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 09:28:33 +08:00
|
|
|
|
function isEmailLike(value) {
|
|
|
|
|
|
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(normalizeText(value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDisplayName(...values) {
|
|
|
|
|
|
for (const value of values) {
|
|
|
|
|
|
const normalized = normalizeText(value)
|
|
|
|
|
|
if (normalized && !isEmailLike(normalized)) {
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
function resolveApplicationApproverName(claim) {
|
|
|
|
|
|
return resolveDisplayName(
|
|
|
|
|
|
claim?.manager_name,
|
|
|
|
|
|
claim?.managerName,
|
|
|
|
|
|
claim?.profile_manager,
|
|
|
|
|
|
claim?.profileManager,
|
|
|
|
|
|
claim?.direct_manager_name,
|
|
|
|
|
|
claim?.directManagerName
|
|
|
|
|
|
) || '直属领导'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
|
function resolveReimbursementApproverName(claim, label) {
|
|
|
|
|
|
const stepLabel = normalizeText(label)
|
|
|
|
|
|
if (stepLabel === '直属领导审批') {
|
|
|
|
|
|
return resolveDisplayName(
|
|
|
|
|
|
claim?.manager_name,
|
|
|
|
|
|
claim?.managerName,
|
|
|
|
|
|
claim?.profile_manager,
|
|
|
|
|
|
claim?.profileManager,
|
|
|
|
|
|
claim?.direct_manager_name,
|
|
|
|
|
|
claim?.directManagerName
|
|
|
|
|
|
) || '直属领导'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '财务审批') {
|
|
|
|
|
|
const routeEvent = findReimbursementFinanceRouteEvent(claim)
|
|
|
|
|
|
return resolveDisplayName(
|
|
|
|
|
|
claim?.finance_approver_name,
|
|
|
|
|
|
claim?.financeApproverName,
|
|
|
|
|
|
routeEvent?.next_approver_name,
|
|
|
|
|
|
routeEvent?.nextApproverName,
|
|
|
|
|
|
routeEvent?.finance_approver_name,
|
|
|
|
|
|
routeEvent?.financeApproverName,
|
|
|
|
|
|
claim?.finance_owner_name,
|
|
|
|
|
|
claim?.financeOwnerName
|
|
|
|
|
|
) || '财务'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return stepLabel.replace(/审批$/, '') || '审批人'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 17:31:27 +08:00
|
|
|
|
function resolveApplicationBudgetApproverName(claim) {
|
|
|
|
|
|
const routeEvent = findApprovalEventForStep(claim, '直属领导审批')
|
|
|
|
|
|
return resolveDisplayName(
|
2026-06-01 17:07:14 +08:00
|
|
|
|
claim?.budget_approver_name,
|
|
|
|
|
|
claim?.budgetApproverName,
|
2026-05-27 17:31:27 +08:00
|
|
|
|
routeEvent?.next_approver_name,
|
|
|
|
|
|
routeEvent?.nextApproverName,
|
|
|
|
|
|
routeEvent?.budget_approver_name,
|
|
|
|
|
|
routeEvent?.budgetApproverName
|
2026-06-01 17:07:14 +08:00
|
|
|
|
) || '预算管理者'
|
2026-05-27 17:31:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta) {
|
2026-05-27 17:31:27 +08:00
|
|
|
|
const normalizedLabel = normalizeText(label)
|
|
|
|
|
|
const workflowNode = normalizeText(claim?.approval_stage || claim?.workflowNode)
|
2026-06-09 08:32:00 +00:00
|
|
|
|
if (
|
|
|
|
|
|
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& approvalMeta.key !== 'completed'
|
|
|
|
|
|
&& (normalizedLabel === '直属领导审批' || normalizedLabel === '财务审批')
|
|
|
|
|
|
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
|
|
|
|
|
|
) {
|
|
|
|
|
|
return `等待 ${resolveReimbursementApproverName(claim, normalizedLabel)} 批复`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
if (
|
|
|
|
|
|
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& approvalMeta.key !== 'completed'
|
2026-05-27 17:31:27 +08:00
|
|
|
|
&& normalizedLabel === '直属领导审批'
|
|
|
|
|
|
&& (
|
|
|
|
|
|
workflowNode.includes('直属领导')
|
|
|
|
|
|
|| workflowNode.includes('领导审批')
|
|
|
|
|
|
|| workflowNode.includes('部门负责人')
|
|
|
|
|
|
|| workflowNode.includes('负责人审批')
|
|
|
|
|
|
)
|
2026-05-27 14:35:17 +08:00
|
|
|
|
) {
|
|
|
|
|
|
return `等待 ${resolveApplicationApproverName(claim)} 批复`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 17:31:27 +08:00
|
|
|
|
if (
|
|
|
|
|
|
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& approvalMeta.key !== 'completed'
|
|
|
|
|
|
&& normalizedLabel === '预算管理者审批'
|
|
|
|
|
|
&& workflowNode.includes('预算')
|
|
|
|
|
|
) {
|
|
|
|
|
|
return `等待 ${resolveApplicationBudgetApproverName(claim)} 批复`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
return label
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
function getRiskFlags(claim) {
|
|
|
|
|
|
return Array.isArray(claim?.risk_flags_json) ? claim.risk_flags_json : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLatestEvent(events) {
|
|
|
|
|
|
const sortedEvents = events
|
|
|
|
|
|
.filter((item) => item && typeof item === 'object')
|
|
|
|
|
|
.map((item) => ({ ...item, eventDate: toDate(item.created_at || item.createdAt) }))
|
|
|
|
|
|
.filter((item) => item.eventDate)
|
|
|
|
|
|
.sort((a, b) => a.eventDate.getTime() - b.eventDate.getTime())
|
|
|
|
|
|
|
|
|
|
|
|
return sortedEvents.length ? sortedEvents[sortedEvents.length - 1] : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findApprovalEventForStep(claim, label) {
|
|
|
|
|
|
const stepLabel = normalizeText(label)
|
|
|
|
|
|
const events = getRiskFlags(claim).filter((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const source = normalizeText(flag.source)
|
2026-05-27 17:31:27 +08:00
|
|
|
|
if (!['manual_approval', 'budget_approval', 'finance_approval'].includes(source)) {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
|
|
|
|
|
|
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '直属领导审批') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
previousStage.includes('直属领导')
|
|
|
|
|
|
|| previousStage.includes('领导审批')
|
2026-05-27 17:31:27 +08:00
|
|
|
|
|| nextStage.includes('预算')
|
2026-05-20 21:00:47 +08:00
|
|
|
|
|| nextStage.includes('财务')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 17:31:27 +08:00
|
|
|
|
if (stepLabel === '预算管理者审批') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
source === 'budget_approval'
|
|
|
|
|
|
|| previousStage.includes('预算')
|
|
|
|
|
|
|| nextStage.includes('审批完成')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
if (stepLabel === '财务审批') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
previousStage.includes('财务')
|
2026-05-28 12:09:49 +08:00
|
|
|
|
|| nextStage.includes('待付款')
|
2026-05-20 21:00:47 +08:00
|
|
|
|
|| nextStage.includes('归档')
|
|
|
|
|
|
|| nextStage.includes('入账')
|
|
|
|
|
|
|| nextStage.includes('完成')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return getLatestEvent(events)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
|
function findReimbursementFinanceRouteEvent(claim) {
|
|
|
|
|
|
return getLatestEvent(
|
|
|
|
|
|
getRiskFlags(claim).filter((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const source = normalizeText(flag.source)
|
|
|
|
|
|
if (!['manual_approval', 'budget_approval'].includes(source)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
|
|
|
|
|
|
return nextStage.includes('财务')
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
function findLatestReturnEvent(claim) {
|
|
|
|
|
|
return getLatestEvent(
|
|
|
|
|
|
getRiskFlags(claim).filter((flag) => (
|
|
|
|
|
|
flag
|
|
|
|
|
|
&& typeof flag === 'object'
|
|
|
|
|
|
&& normalizeText(flag.source) === 'manual_return'
|
|
|
|
|
|
))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
function findLatestPaymentEvent(claim) {
|
|
|
|
|
|
return getLatestEvent(
|
|
|
|
|
|
getRiskFlags(claim).filter((flag) => (
|
|
|
|
|
|
flag
|
|
|
|
|
|
&& typeof flag === 'object'
|
|
|
|
|
|
&& (
|
|
|
|
|
|
normalizeText(flag.source) === 'payment'
|
|
|
|
|
|
|| normalizeText(flag.event_type || flag.eventType) === 'expense_claim_payment_completed'
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
function normalizeApplicationHandoffDetail(flag = {}) {
|
|
|
|
|
|
const detail = flag?.application_detail || flag?.applicationDetail || {}
|
|
|
|
|
|
const reviewValues = flag?.review_form_values || flag?.reviewFormValues || {}
|
|
|
|
|
|
const sceneSelection = flag?.expense_scene_selection || flag?.expenseSceneSelection || {}
|
|
|
|
|
|
return [sceneSelection, reviewValues, detail]
|
|
|
|
|
|
.filter((item) => item && typeof item === 'object')
|
|
|
|
|
|
.reduce((acc, item) => ({ ...acc, ...item }), {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveApplicationField(flag = {}, detail = {}, snakeKey, camelKey = '') {
|
|
|
|
|
|
return normalizeText(
|
|
|
|
|
|
flag?.[snakeKey]
|
|
|
|
|
|
|| (camelKey ? flag?.[camelKey] : '')
|
|
|
|
|
|
|| detail?.[snakeKey]
|
|
|
|
|
|
|| (camelKey ? detail?.[camelKey] : '')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
|
function resolveApplicationValue(flag = {}, detail = {}, keys = []) {
|
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
|
const detailValue = normalizeText(detail?.[key])
|
|
|
|
|
|
if (detailValue) {
|
|
|
|
|
|
return detailValue
|
|
|
|
|
|
}
|
|
|
|
|
|
const flagValue = normalizeText(flag?.[key])
|
|
|
|
|
|
if (flagValue) {
|
|
|
|
|
|
return flagValue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractDateRange(value) {
|
|
|
|
|
|
const dates = normalizeText(value).match(/\d{4}-\d{2}-\d{2}/g) || []
|
|
|
|
|
|
if (!dates.length) {
|
|
|
|
|
|
return { startDate: '', endDate: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
startDate: dates[0],
|
|
|
|
|
|
endDate: dates[dates.length - 1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
function resolveRelatedApplicationClaimNo(flag = {}) {
|
|
|
|
|
|
const detail = normalizeApplicationHandoffDetail(flag)
|
|
|
|
|
|
return resolveApplicationField(flag, detail, 'application_claim_no', 'applicationClaimNo')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findRelatedApplicationEvent(claim) {
|
|
|
|
|
|
const events = getRiskFlags(claim).filter((flag) => (
|
2026-05-30 15:46:51 +08:00
|
|
|
|
flag
|
|
|
|
|
|
&& typeof flag === 'object'
|
2026-06-01 17:07:14 +08:00
|
|
|
|
&& resolveRelatedApplicationClaimNo(flag)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
))
|
2026-06-01 17:07:14 +08:00
|
|
|
|
return getLatestEvent(events) || events[events.length - 1] || null
|
2026-05-30 15:46:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
|
|
|
|
|
|
const explicitLabel = normalizeText(
|
|
|
|
|
|
flag?.application_amount_label
|
|
|
|
|
|
|| flag?.applicationAmountLabel
|
|
|
|
|
|
|| detail?.application_amount_label
|
|
|
|
|
|
|| detail?.applicationAmountLabel
|
|
|
|
|
|
)
|
|
|
|
|
|
if (explicitLabel) return explicitLabel
|
|
|
|
|
|
|
|
|
|
|
|
const rawAmount = normalizeText(
|
|
|
|
|
|
flag?.application_amount
|
|
|
|
|
|
|| flag?.applicationAmount
|
|
|
|
|
|
|| flag?.application_budget_amount
|
|
|
|
|
|
|| flag?.applicationBudgetAmount
|
|
|
|
|
|
|| detail?.application_amount
|
|
|
|
|
|
|| detail?.applicationAmount
|
|
|
|
|
|
|| detail?.amount
|
|
|
|
|
|
|| claim?.amount
|
|
|
|
|
|
)
|
|
|
|
|
|
const amountValue = parseNumber(rawAmount)
|
|
|
|
|
|
return amountValue > 0 ? formatAmount(amountValue) : rawAmount
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const relatedEvent = findRelatedApplicationEvent(claim)
|
|
|
|
|
|
if (!relatedEvent) {
|
2026-05-30 15:46:51 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const detail = normalizeApplicationHandoffDetail(relatedEvent)
|
|
|
|
|
|
const claimNo = resolveRelatedApplicationClaimNo(relatedEvent)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const applicationType = normalizeText(
|
|
|
|
|
|
detail.application_type
|
|
|
|
|
|
|| detail.applicationType
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_type
|
|
|
|
|
|
|| relatedEvent.applicationType
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|| typeLabel
|
|
|
|
|
|
)
|
|
|
|
|
|
const location = normalizeText(
|
|
|
|
|
|
detail.application_location
|
|
|
|
|
|
|| detail.applicationLocation
|
|
|
|
|
|
|| detail.location
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_location
|
|
|
|
|
|
|| relatedEvent.applicationLocation
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|| claim?.location
|
|
|
|
|
|
)
|
|
|
|
|
|
const reason = normalizeText(
|
|
|
|
|
|
detail.application_reason
|
|
|
|
|
|
|| detail.applicationReason
|
|
|
|
|
|
|| detail.reason
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_reason
|
|
|
|
|
|
|| relatedEvent.applicationReason
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|| claim?.reason
|
|
|
|
|
|
)
|
|
|
|
|
|
const content = normalizeText(
|
|
|
|
|
|
detail.application_content
|
|
|
|
|
|
|| detail.applicationContent
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_content
|
|
|
|
|
|
|| relatedEvent.applicationContent
|
2026-05-30 15:46:51 +08:00
|
|
|
|
) || [applicationType, location].filter(Boolean).join(' / ')
|
|
|
|
|
|
const rawTime = normalizeText(
|
|
|
|
|
|
detail.application_time
|
|
|
|
|
|
|| detail.applicationTime
|
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-05-30 15:46:51 +08:00
|
|
|
|
|| detail.time
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| detail.application_date
|
|
|
|
|
|
|| detail.applicationDate
|
|
|
|
|
|
|| relatedEvent.application_time
|
|
|
|
|
|
|| relatedEvent.applicationTime
|
2026-06-02 16:22:59 +08:00
|
|
|
|
|| relatedEvent.application_business_time
|
|
|
|
|
|
|| relatedEvent.applicationBusinessTime
|
|
|
|
|
|
|| relatedEvent.business_time
|
|
|
|
|
|
|| relatedEvent.businessTime
|
|
|
|
|
|
|| relatedEvent.time_range
|
|
|
|
|
|
|| relatedEvent.timeRange
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_date
|
|
|
|
|
|
|| relatedEvent.applicationDate
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|| claim?.occurred_at
|
|
|
|
|
|
)
|
2026-06-02 16:22:59 +08:00
|
|
|
|
const displayTime = formatDate(rawTime) || rawTime
|
|
|
|
|
|
const dateRange = extractDateRange(rawTime || displayTime)
|
|
|
|
|
|
const ruleName = resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_rule_name',
|
|
|
|
|
|
'applicationRuleName',
|
|
|
|
|
|
'rule_name',
|
|
|
|
|
|
'ruleName'
|
|
|
|
|
|
])
|
|
|
|
|
|
const ruleVersion = resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_rule_version',
|
|
|
|
|
|
'applicationRuleVersion',
|
|
|
|
|
|
'rule_version',
|
|
|
|
|
|
'ruleVersion'
|
|
|
|
|
|
])
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
id: resolveApplicationField(relatedEvent, detail, 'application_claim_id', 'applicationClaimId'),
|
2026-05-30 15:46:51 +08:00
|
|
|
|
claimNo,
|
|
|
|
|
|
content,
|
|
|
|
|
|
reason,
|
|
|
|
|
|
days: normalizeText(
|
|
|
|
|
|
detail.application_days
|
|
|
|
|
|
|| detail.applicationDays
|
|
|
|
|
|
|| detail.days
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_days
|
|
|
|
|
|
|| relatedEvent.applicationDays
|
2026-05-30 15:46:51 +08:00
|
|
|
|
),
|
|
|
|
|
|
location,
|
2026-06-02 16:22:59 +08:00
|
|
|
|
time: displayTime,
|
|
|
|
|
|
tripStartDate: dateRange.startDate,
|
|
|
|
|
|
tripEndDate: dateRange.endDate,
|
2026-06-01 17:07:14 +08:00
|
|
|
|
amountLabel: resolveRelatedApplicationAmountLabel(relatedEvent, detail, claim),
|
|
|
|
|
|
statusLabel: resolveApplicationField(relatedEvent, detail, 'application_status_label', 'applicationStatusLabel'),
|
2026-05-30 15:46:51 +08:00
|
|
|
|
transportMode: normalizeText(
|
|
|
|
|
|
detail.application_transport_mode
|
|
|
|
|
|
|| detail.applicationTransportMode
|
|
|
|
|
|
|| detail.transport_mode
|
2026-06-01 17:07:14 +08:00
|
|
|
|
|| relatedEvent.application_transport_mode
|
|
|
|
|
|
|| relatedEvent.applicationTransportMode
|
2026-06-02 16:22:59 +08:00
|
|
|
|
),
|
|
|
|
|
|
lodgingDailyCap: resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_lodging_daily_cap',
|
|
|
|
|
|
'applicationLodgingDailyCap',
|
|
|
|
|
|
'lodging_daily_cap',
|
|
|
|
|
|
'lodgingDailyCap'
|
|
|
|
|
|
]),
|
|
|
|
|
|
subsidyDailyCap: resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_subsidy_daily_cap',
|
|
|
|
|
|
'applicationSubsidyDailyCap',
|
|
|
|
|
|
'subsidy_daily_cap',
|
|
|
|
|
|
'subsidyDailyCap'
|
|
|
|
|
|
]),
|
|
|
|
|
|
transportPolicy: resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_transport_policy',
|
|
|
|
|
|
'applicationTransportPolicy',
|
|
|
|
|
|
'transport_policy',
|
|
|
|
|
|
'transportPolicy'
|
|
|
|
|
|
]),
|
|
|
|
|
|
policyEstimate: resolveApplicationValue(relatedEvent, detail, [
|
|
|
|
|
|
'application_policy_estimate',
|
|
|
|
|
|
'applicationPolicyEstimate',
|
|
|
|
|
|
'policy_estimate',
|
|
|
|
|
|
'policyEstimate'
|
|
|
|
|
|
]),
|
|
|
|
|
|
ruleName,
|
|
|
|
|
|
ruleVersion,
|
|
|
|
|
|
ruleLabel: [ruleName, ruleVersion].filter(Boolean).join(' / ')
|
2026-05-30 15:46:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
|
function findLatestApplicationReturnEvent(claim) {
|
|
|
|
|
|
return getLatestEvent(
|
|
|
|
|
|
getRiskFlags(claim).filter((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object' || normalizeText(flag.source) !== 'manual_return') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const eventType = normalizeText(flag.event_type || flag.eventType)
|
|
|
|
|
|
const returnStage = normalizeText(flag.return_stage || flag.returnStage || flag.previous_approval_stage)
|
|
|
|
|
|
const stageKey = normalizeText(flag.return_stage_key || flag.returnStageKey)
|
|
|
|
|
|
return (
|
|
|
|
|
|
eventType === 'expense_application_return'
|
|
|
|
|
|
|| stageKey === 'direct_manager'
|
|
|
|
|
|
|| returnStage.includes('直属领导')
|
|
|
|
|
|
|| returnStage.includes('领导审批')
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
function findMergedApplicationBudgetApprovalEvent(claim) {
|
|
|
|
|
|
return getLatestEvent(
|
|
|
|
|
|
getRiskFlags(claim).filter((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const source = normalizeText(flag.source)
|
|
|
|
|
|
const eventType = normalizeText(flag.event_type || flag.eventType)
|
|
|
|
|
|
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
|
|
|
|
|
|
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
|
|
|
|
|
|
const mergedFlag = Boolean(flag.budget_approval_merged || flag.budgetApprovalMerged)
|
|
|
|
|
|
return (
|
|
|
|
|
|
source === 'manual_approval'
|
|
|
|
|
|
&& eventType === 'expense_application_approval'
|
|
|
|
|
|
&& previousStage.includes('直属领导')
|
2026-06-09 08:32:00 +00:00
|
|
|
|
&& (
|
|
|
|
|
|
nextStage.includes('审批完成')
|
|
|
|
|
|
|| nextStage.includes(APPLICATION_LINK_STATUS_STEP_LABEL)
|
|
|
|
|
|
|| nextStage.includes('申请完成')
|
|
|
|
|
|
)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
&& mergedFlag
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
|
function resolveBudgetRouteResult(flag, routeDecision = {}) {
|
|
|
|
|
|
if (routeDecision && typeof routeDecision === 'object') {
|
|
|
|
|
|
const routeBudgetResult = routeDecision.budget_result || routeDecision.budgetResult
|
|
|
|
|
|
if (routeBudgetResult && typeof routeBudgetResult === 'object') {
|
|
|
|
|
|
return routeBudgetResult
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const flagBudgetResult = flag?.budget_result || flag?.budgetResult
|
|
|
|
|
|
return flagBudgetResult && typeof flagBudgetResult === 'object' ? flagBudgetResult : {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applicationBudgetRouteMeetsThreshold(flag, routeDecision = {}) {
|
|
|
|
|
|
const budgetResult = resolveBudgetRouteResult(flag, routeDecision)
|
|
|
|
|
|
const metrics = budgetResult.metrics && typeof budgetResult.metrics === 'object' ? budgetResult.metrics : {}
|
|
|
|
|
|
const overBudgetAmount = parseNumber(metrics.over_budget_amount ?? metrics.overBudgetAmount)
|
|
|
|
|
|
const afterUsageRate = parseNumber(metrics.after_usage_rate ?? metrics.afterUsageRate)
|
|
|
|
|
|
const claimAmountRatio = parseNumber(metrics.claim_amount_ratio ?? metrics.claimAmountRatio)
|
|
|
|
|
|
|
|
|
|
|
|
return overBudgetAmount > 0 || Math.max(afterUsageRate, claimAmountRatio) >= 90
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
function applicationRequiresBudgetReviewStep(claim, workflowNode) {
|
|
|
|
|
|
const node = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
|
|
|
|
|
|
if (node.includes('预算')) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return getRiskFlags(claim).some((flag) => {
|
|
|
|
|
|
if (!flag || typeof flag !== 'object') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const source = normalizeText(flag.source)
|
|
|
|
|
|
const eventType = normalizeText(flag.event_type || flag.eventType)
|
|
|
|
|
|
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
|
|
|
|
|
|
const routeDecision = flag.route_decision || flag.routeDecision || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (source === 'approval_routing' && flag.requires_budget_review === true) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return applicationBudgetRouteMeetsThreshold(flag, flag)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
routeDecision
|
|
|
|
|
|
&& typeof routeDecision === 'object'
|
|
|
|
|
|
&& routeDecision.requires_budget_review === true
|
|
|
|
|
|
) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
return applicationBudgetRouteMeetsThreshold(flag, routeDecision)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
source === 'budget_approval'
|
|
|
|
|
|
|| eventType === 'expense_application_budget_approval'
|
|
|
|
|
|
|| previousStage.includes('预算')
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
function buildProgressStepMeta(time, detail = '', title = '') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
time,
|
|
|
|
|
|
detail,
|
|
|
|
|
|
title: title || [time, detail].filter(Boolean).join(' ')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildCompletedStepMeta(claim, label) {
|
|
|
|
|
|
const stepLabel = normalizeText(label)
|
|
|
|
|
|
const employeeName = normalizeText(claim?.employee_name) || '申请人'
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (stepLabel === RELATED_APPLICATION_STEP_LABEL) {
|
|
|
|
|
|
const relatedApplication = resolveRelatedApplicationInfo(claim)
|
|
|
|
|
|
const createdAt = formatDateTime(claim?.created_at)
|
|
|
|
|
|
if (relatedApplication?.claimNo) {
|
|
|
|
|
|
return buildProgressStepMeta(`已关联 ${relatedApplication.claimNo}`, createdAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
return buildProgressStepMeta('待核对关联单据', createdAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (stepLabel === APPLICATION_LINK_STATUS_STEP_LABEL) {
|
|
|
|
|
|
return buildApplicationLinkStatusStepMeta(claim)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const createdAt = formatDateTime(claim?.created_at)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
return buildProgressStepMeta(stepLabel === '创建申请' ? `${employeeName}发起申请` : `${employeeName}创建`, createdAt)
|
2026-05-20 21:00:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '待提交') {
|
|
|
|
|
|
const submittedAt = formatDateTime(claim?.submitted_at)
|
|
|
|
|
|
return buildProgressStepMeta(`${employeeName}提交`, submittedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 17:31:27 +08:00
|
|
|
|
if (stepLabel === '直属领导审批' || stepLabel === '预算管理者审批' || stepLabel === '财务审批') {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const approvalEvent = findApprovalEventForStep(claim, stepLabel)
|
|
|
|
|
|
if (approvalEvent) {
|
2026-05-21 09:28:33 +08:00
|
|
|
|
const operator = resolveDisplayName(
|
|
|
|
|
|
approvalEvent.operator,
|
|
|
|
|
|
approvalEvent.operator_name,
|
|
|
|
|
|
approvalEvent.operatorName,
|
2026-05-27 17:31:27 +08:00
|
|
|
|
stepLabel === '直属领导审批' ? claim?.manager_name : '',
|
|
|
|
|
|
stepLabel === '预算管理者审批' ? approvalEvent.next_approver_name : ''
|
2026-06-01 17:07:14 +08:00
|
|
|
|
) || (stepLabel === '财务审批' ? '财务' : stepLabel === '预算管理者审批' ? '预算管理者' : '直属领导')
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const approvedAt = formatDateTime(approvalEvent.created_at || approvalEvent.createdAt)
|
|
|
|
|
|
return buildProgressStepMeta(`${operator}通过`, approvedAt, `${operator}审批通过 ${approvedAt}`.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '财务审批') {
|
|
|
|
|
|
const updatedAt = formatDateTime(claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta('财务通过', updatedAt, `财务审批通过 ${updatedAt}`.trim())
|
|
|
|
|
|
}
|
2026-05-27 14:35:17 +08:00
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '直属领导审批') {
|
|
|
|
|
|
const returnEvent = findLatestApplicationReturnEvent(claim)
|
|
|
|
|
|
if (returnEvent) {
|
|
|
|
|
|
const handledAt = formatDateTime(returnEvent.created_at || returnEvent.createdAt)
|
|
|
|
|
|
return buildProgressStepMeta('已处理', handledAt, `直属领导已处理 ${handledAt}`.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '退回') {
|
|
|
|
|
|
const returnEvent = findLatestApplicationReturnEvent(claim) || findLatestReturnEvent(claim)
|
|
|
|
|
|
if (returnEvent) {
|
|
|
|
|
|
const operator = resolveDisplayName(
|
|
|
|
|
|
returnEvent.operator,
|
|
|
|
|
|
returnEvent.operator_name,
|
|
|
|
|
|
returnEvent.operatorName,
|
|
|
|
|
|
claim?.manager_name
|
|
|
|
|
|
) || '直属领导'
|
|
|
|
|
|
const returnedAt = formatDateTime(returnEvent.created_at || returnEvent.createdAt)
|
|
|
|
|
|
return buildProgressStepMeta(`${operator}退回`, returnedAt, `${operator}退回 ${returnedAt}`.trim())
|
|
|
|
|
|
}
|
2026-05-20 21:00:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (stepLabel === '待付款') {
|
|
|
|
|
|
const approvalEvent = findApprovalEventForStep(claim, '财务审批')
|
|
|
|
|
|
const pendingAt = formatDateTime(approvalEvent?.created_at || approvalEvent?.createdAt || claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta('待付款', pendingAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stepLabel === '已付款') {
|
|
|
|
|
|
const paymentEvent = findLatestPaymentEvent(claim)
|
|
|
|
|
|
const paidAt = formatDateTime(paymentEvent?.created_at || paymentEvent?.createdAt || claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta('已付款', paidAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
if (stepLabel === '归档入账') {
|
|
|
|
|
|
const archivedAt = formatDateTime(claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta('归档入账', archivedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (stepLabel === ARCHIVED_STEP_LABEL) {
|
|
|
|
|
|
const archivedAt = formatDateTime(claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta(ARCHIVED_STEP_LABEL, archivedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
if (stepLabel === '审批完成') {
|
|
|
|
|
|
const completedAt = formatDateTime(claim?.updated_at)
|
|
|
|
|
|
return buildProgressStepMeta('审批完成', completedAt)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:00:47 +08:00
|
|
|
|
return buildProgressStepMeta('已完成')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveCurrentStepStartedAt(claim, label) {
|
|
|
|
|
|
const stepLabel = normalizeText(label)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (stepLabel === RELATED_APPLICATION_STEP_LABEL || stepLabel === '创建单据' || stepLabel === '创建申请') {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
return claim?.created_at
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stepLabel === '待提交') {
|
|
|
|
|
|
const returnEvent = findLatestReturnEvent(claim)
|
|
|
|
|
|
return returnEvent?.created_at || returnEvent?.createdAt || claim?.updated_at || claim?.created_at
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stepLabel === '直属领导审批') {
|
|
|
|
|
|
return claim?.submitted_at || claim?.updated_at || claim?.created_at
|
|
|
|
|
|
}
|
2026-05-27 17:31:27 +08:00
|
|
|
|
if (stepLabel === '预算管理者审批') {
|
|
|
|
|
|
const leaderApprovalEvent = findApprovalEventForStep(claim, '直属领导审批')
|
|
|
|
|
|
return leaderApprovalEvent?.created_at || leaderApprovalEvent?.createdAt || claim?.updated_at || claim?.submitted_at
|
|
|
|
|
|
}
|
2026-05-20 21:00:47 +08:00
|
|
|
|
if (stepLabel === '财务审批') {
|
|
|
|
|
|
const leaderApprovalEvent = findApprovalEventForStep(claim, '直属领导审批')
|
|
|
|
|
|
return leaderApprovalEvent?.created_at || leaderApprovalEvent?.createdAt || claim?.updated_at || claim?.submitted_at
|
|
|
|
|
|
}
|
2026-05-28 12:09:49 +08:00
|
|
|
|
if (stepLabel === '待付款') {
|
|
|
|
|
|
const approvalEvent = findApprovalEventForStep(claim, '财务审批')
|
|
|
|
|
|
return approvalEvent?.created_at || approvalEvent?.createdAt || claim?.updated_at || claim?.submitted_at
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stepLabel === '已付款') {
|
|
|
|
|
|
const paymentEvent = findLatestPaymentEvent(claim)
|
|
|
|
|
|
return paymentEvent?.created_at || paymentEvent?.createdAt || claim?.updated_at || claim?.submitted_at
|
|
|
|
|
|
}
|
2026-05-30 15:46:51 +08:00
|
|
|
|
if (stepLabel === '归档入账' || stepLabel === ARCHIVED_STEP_LABEL || stepLabel === '审批完成') {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
return claim?.updated_at || claim?.submitted_at
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}) {
|
|
|
|
|
|
const documentTypeCode = String(options.documentTypeCode || '').trim()
|
2026-05-27 14:35:17 +08:00
|
|
|
|
const hasApplicationReturnStep = (
|
|
|
|
|
|
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& Boolean(findLatestApplicationReturnEvent(claim))
|
|
|
|
|
|
&& approvalMeta.key === 'supplement'
|
|
|
|
|
|
)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const hasMergedApplicationBudgetApproval = (
|
|
|
|
|
|
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& Boolean(findMergedApplicationBudgetApprovalEvent(claim))
|
|
|
|
|
|
)
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const shouldShowApplicationBudgetStep = (
|
|
|
|
|
|
documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
&& !hasMergedApplicationBudgetApproval
|
|
|
|
|
|
&& applicationRequiresBudgetReviewStep(claim, workflowNode)
|
|
|
|
|
|
)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const isApplicationDocument = documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
|
|
|
|
|
const applicationArchived = isApplicationDocument && isApplicationArchivedWorkflow(claim, workflowNode)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const progressLabels =
|
2026-06-06 17:19:07 +08:00
|
|
|
|
isApplicationDocument
|
2026-05-27 14:35:17 +08:00
|
|
|
|
? hasApplicationReturnStep
|
|
|
|
|
|
? ['创建申请', '直属领导审批', '退回', '待提交']
|
2026-05-30 15:46:51 +08:00
|
|
|
|
: hasMergedApplicationBudgetApproval
|
2026-06-09 08:32:00 +00:00
|
|
|
|
? ['创建申请', '直属领导审批', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
|
2026-06-01 17:07:14 +08:00
|
|
|
|
: shouldShowApplicationBudgetStep
|
|
|
|
|
|
? APPLICATION_PROGRESS_LABELS
|
|
|
|
|
|
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
|
2026-05-25 13:35:39 +08:00
|
|
|
|
: REIMBURSEMENT_PROGRESS_LABELS
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const applicationLinkIndex = progressLabels.indexOf(APPLICATION_LINK_STATUS_STEP_LABEL)
|
|
|
|
|
|
const applicationArchiveIndex = progressLabels.indexOf(ARCHIVED_STEP_LABEL)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const currentIndex =
|
2026-06-06 17:19:07 +08:00
|
|
|
|
isApplicationDocument
|
2026-05-27 14:35:17 +08:00
|
|
|
|
? hasApplicationReturnStep
|
|
|
|
|
|
? 3
|
2026-06-06 17:19:07 +08:00
|
|
|
|
: applicationArchived && applicationArchiveIndex >= 0
|
|
|
|
|
|
? applicationArchiveIndex
|
|
|
|
|
|
: approvalMeta.key === 'completed' && applicationLinkIndex >= 0
|
|
|
|
|
|
? applicationLinkIndex
|
|
|
|
|
|
: Math.min(
|
|
|
|
|
|
resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode),
|
|
|
|
|
|
Math.max(0, progressLabels.length - 1)
|
|
|
|
|
|
)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
: resolveProgressCurrentIndex(approvalMeta, workflowNode)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const currentTime =
|
|
|
|
|
|
approvalMeta.key === 'completed'
|
|
|
|
|
|
? '已完成'
|
2026-05-28 12:09:49 +08:00
|
|
|
|
: approvalMeta.key === 'pending_payment'
|
|
|
|
|
|
? '待付款'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
: approvalMeta.key === 'supplement'
|
|
|
|
|
|
? '待补充'
|
|
|
|
|
|
: approvalMeta.key === 'rejected'
|
|
|
|
|
|
? '已退回'
|
|
|
|
|
|
: '进行中'
|
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
|
return progressLabels.map((label, index) => {
|
2026-05-27 14:35:17 +08:00
|
|
|
|
const displayLabel = resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (approvalMeta.key === 'completed' && (!isApplicationDocument || applicationArchived)) {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const stepMeta = buildCompletedStepMeta(claim, label)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return {
|
|
|
|
|
|
index: index + 1,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
label: displayLabel,
|
|
|
|
|
|
rawLabel: label,
|
2026-05-20 21:00:47 +08:00
|
|
|
|
time: stepMeta.time,
|
|
|
|
|
|
detail: stepMeta.detail,
|
|
|
|
|
|
title: stepMeta.title,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
done: true,
|
|
|
|
|
|
active: true,
|
|
|
|
|
|
current: false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (index < currentIndex) {
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const stepMeta = buildCompletedStepMeta(claim, label)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return {
|
|
|
|
|
|
index: index + 1,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
label: displayLabel,
|
|
|
|
|
|
rawLabel: label,
|
2026-05-20 21:00:47 +08:00
|
|
|
|
time: stepMeta.time,
|
|
|
|
|
|
detail: stepMeta.detail,
|
|
|
|
|
|
title: stepMeta.title,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
done: true,
|
|
|
|
|
|
active: true,
|
|
|
|
|
|
current: false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (index === currentIndex) {
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (isApplicationDocument && label === APPLICATION_LINK_STATUS_STEP_LABEL) {
|
|
|
|
|
|
const stepMeta = buildApplicationLinkStatusStepMeta(claim)
|
|
|
|
|
|
return {
|
|
|
|
|
|
index: index + 1,
|
|
|
|
|
|
label: displayLabel,
|
|
|
|
|
|
rawLabel: label,
|
|
|
|
|
|
time: stepMeta.time,
|
|
|
|
|
|
detail: stepMeta.detail,
|
|
|
|
|
|
title: stepMeta.title,
|
|
|
|
|
|
done: false,
|
|
|
|
|
|
active: true,
|
|
|
|
|
|
current: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-20 21:00:47 +08:00
|
|
|
|
const stayDuration = formatDurationFrom(resolveCurrentStepStartedAt(claim, label))
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return {
|
|
|
|
|
|
index: index + 1,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
label: displayLabel,
|
|
|
|
|
|
rawLabel: label,
|
2026-05-20 21:00:47 +08:00
|
|
|
|
time: stayDuration ? `停留 ${stayDuration}` : currentTime,
|
|
|
|
|
|
detail: '',
|
2026-05-27 14:35:17 +08:00
|
|
|
|
title: stayDuration ? `当前${displayLabel}已停留 ${stayDuration}` : currentTime,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
done: false,
|
|
|
|
|
|
active: true,
|
|
|
|
|
|
current: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
index: index + 1,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
label: displayLabel,
|
|
|
|
|
|
rawLabel: label,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
time: '待处理',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
detail: '',
|
|
|
|
|
|
title: '待处理',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
done: false,
|
|
|
|
|
|
active: false,
|
|
|
|
|
|
current: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
|
function buildExpenseItems(claim, riskMeta) {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (!Array.isArray(claim?.items)) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const normalizedRiskMeta = typeof riskMeta === 'string'
|
|
|
|
|
|
? { summary: riskMeta, tone: riskMeta === '无' ? 'low' : 'medium', label: riskMeta === '无' ? '无' : '待关注' }
|
|
|
|
|
|
: {
|
|
|
|
|
|
summary: String(riskMeta?.summary || '无').trim() || '无',
|
|
|
|
|
|
tone: String(riskMeta?.tone || 'low').trim() || 'low',
|
|
|
|
|
|
label: String(riskMeta?.label || '').trim() || (String(riskMeta?.summary || '').trim() === '无' ? '无' : '待关注')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 16:22:59 +08:00
|
|
|
|
const visibleItems = filterVisibleExpenseRawItems(claim.items, claim)
|
|
|
|
|
|
const sortedItems = [...visibleItems].sort((left, right) => {
|
2026-05-21 10:57:06 +08:00
|
|
|
|
const leftType = normalizeExpenseType(left?.item_type)
|
|
|
|
|
|
const rightType = normalizeExpenseType(right?.item_type)
|
|
|
|
|
|
return Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(leftType)) - Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(rightType))
|
|
|
|
|
|
})
|
|
|
|
|
|
const travelTimeLabelMap = buildTravelTimeLabelMap(sortedItems, claim)
|
2026-06-03 17:31:40 +08:00
|
|
|
|
const standardAdjustmentMap = buildStandardAdjustmentMapFromClaim(claim)
|
2026-05-21 10:57:06 +08:00
|
|
|
|
|
|
|
|
|
|
return sortedItems.map((item, index) => {
|
2026-05-13 06:48:27 +00:00
|
|
|
|
const invoiceId = String(item?.invoice_id || '').trim()
|
|
|
|
|
|
const attachmentName = resolveAttachmentDisplayName(invoiceId)
|
|
|
|
|
|
const attachments = invoiceId ? [attachmentName || invoiceId] : []
|
|
|
|
|
|
const itemType = normalizeExpenseType(item?.item_type || claim?.expense_type)
|
2026-05-21 10:57:06 +08:00
|
|
|
|
const isSystemGenerated = Boolean(item?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType))
|
|
|
|
|
|
const id = resolveExpenseItemViewId(item, index, claim)
|
2026-05-13 06:48:27 +00:00
|
|
|
|
const itemTypeLabel = resolveTypeLabel(itemType)
|
|
|
|
|
|
const itemLocation = String(item?.item_location || '').trim()
|
|
|
|
|
|
const itemReason = String(item?.item_reason || '').trim()
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const itemNote = String(item?.item_note || item?.itemNote || '').trim()
|
2026-05-13 06:48:27 +00:00
|
|
|
|
const itemAmount = parseNumber(item?.item_amount)
|
|
|
|
|
|
const itemAmountDisplay = itemAmount > 0 ? formatAmount(itemAmount) : '待补充'
|
2026-06-03 17:31:40 +08:00
|
|
|
|
const standardAdjustment = standardAdjustmentMap.get(id) || null
|
|
|
|
|
|
const originalItemAmount = standardAdjustment?.originalAmount ?? itemAmount
|
|
|
|
|
|
const reimbursableAmount = standardAdjustment?.reimbursableAmount ?? itemAmount
|
|
|
|
|
|
const employeeAbsorbedAmount = standardAdjustment?.employeeAbsorbedAmount || Math.max(originalItemAmount - reimbursableAmount, 0)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-21 10:57:06 +08:00
|
|
|
|
id,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
time: formatDate(item?.item_date) || '待补充',
|
|
|
|
|
|
itemDate: formatDate(item?.item_date) || '',
|
2026-05-20 21:00:47 +08:00
|
|
|
|
filledAt: formatDateTime(item?.created_at) || '待同步',
|
2026-05-13 06:48:27 +00:00
|
|
|
|
itemType,
|
|
|
|
|
|
itemReason,
|
|
|
|
|
|
itemLocation,
|
2026-06-03 15:46:56 +08:00
|
|
|
|
itemNote,
|
2026-05-13 06:48:27 +00:00
|
|
|
|
itemAmount,
|
2026-06-03 17:31:40 +08:00
|
|
|
|
originalItemAmount,
|
|
|
|
|
|
originalAmountDisplay: originalItemAmount > 0 ? formatAmount(originalItemAmount) : itemAmountDisplay,
|
|
|
|
|
|
reimbursableAmount,
|
|
|
|
|
|
reimbursableAmountDisplay: reimbursableAmount > 0 ? formatAmount(reimbursableAmount) : '待补充',
|
|
|
|
|
|
employeeAbsorbedAmount,
|
|
|
|
|
|
employeeAbsorbedAmountDisplay: employeeAbsorbedAmount > 0 ? formatAmount(employeeAbsorbedAmount) : '',
|
|
|
|
|
|
hasStandardAdjustment: reimbursableAmount >= 0 && reimbursableAmount < originalItemAmount,
|
|
|
|
|
|
standardAdjustmentAccepted: Boolean(standardAdjustment),
|
|
|
|
|
|
standardAdjustmentMessage: standardAdjustment?.message || '',
|
2026-05-13 06:48:27 +00:00
|
|
|
|
invoiceId,
|
2026-05-21 10:57:06 +08:00
|
|
|
|
isSystemGenerated,
|
|
|
|
|
|
dayLabel: resolveExpenseTimeLabel({
|
|
|
|
|
|
id,
|
|
|
|
|
|
itemType,
|
|
|
|
|
|
isSystemGenerated,
|
|
|
|
|
|
claim,
|
|
|
|
|
|
travelTimeLabelMap
|
|
|
|
|
|
}),
|
2026-05-13 03:29:10 +00:00
|
|
|
|
name: itemTypeLabel,
|
|
|
|
|
|
category: itemTypeLabel,
|
2026-05-13 06:48:27 +00:00
|
|
|
|
desc: itemReason || '待补充',
|
2026-05-22 16:00:19 +08:00
|
|
|
|
detail: resolveExpenseDescriptionDetail(itemType, itemLocation),
|
2026-05-13 06:48:27 +00:00
|
|
|
|
amount: itemAmountDisplay,
|
2026-05-21 10:57:06 +08:00
|
|
|
|
status: isSystemGenerated ? '系统计算' : attachments.length ? '已识别' : '待补充',
|
|
|
|
|
|
tone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'bad',
|
|
|
|
|
|
attachmentStatus: isSystemGenerated ? '无需附件' : attachments.length ? '已关联票据' : '未上传',
|
|
|
|
|
|
attachmentHint: isSystemGenerated ? '根据出差天数与职级自动测算' : attachments.length ? attachments[0] : '仅支持上传 1 张 JPG、PNG、PDF 单据',
|
|
|
|
|
|
attachmentTone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'missing',
|
2026-05-13 03:29:10 +00:00
|
|
|
|
attachments,
|
2026-06-03 15:46:56 +08:00
|
|
|
|
riskLabel: normalizedRiskMeta.summary === '无' ? '无' : normalizedRiskMeta.label,
|
|
|
|
|
|
riskText: normalizedRiskMeta.summary === '无' ? '' : normalizedRiskMeta.summary,
|
|
|
|
|
|
riskTone: normalizedRiskMeta.summary === '无' ? 'low' : normalizedRiskMeta.tone
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:56:51 +00:00
|
|
|
|
export function mapExpenseClaimToRequest(claim) {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const typeCode = String(claim?.expense_type || '').trim() || 'other'
|
|
|
|
|
|
const typeLabel = resolveTypeLabel(typeCode)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const documentTypeMeta = resolveDocumentTypeMeta(claim, typeCode)
|
|
|
|
|
|
const isApplicationDocument = documentTypeMeta.documentTypeCode === DOCUMENT_TYPE_APPLICATION
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const approvalMeta = resolveApprovalMeta(claim?.status)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const workflowNode = resolveWorkflowNode(claim, approvalMeta, isApplicationDocument)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const applicationArchived = isApplicationDocument && isApplicationArchivedWorkflow(claim, workflowNode)
|
|
|
|
|
|
const applicationLinkedReimbursementNo = isApplicationDocument ? resolveApplicationLinkedReimbursementNo(claim) : ''
|
|
|
|
|
|
const applicationLinkStatusText = applicationLinkedReimbursementNo
|
|
|
|
|
|
? `关联中 ${applicationLinkedReimbursementNo}`
|
|
|
|
|
|
: '未关联'
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count))
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const riskMeta = buildRiskMeta(claim?.risk_flags_json)
|
|
|
|
|
|
const riskSummary = riskMeta.summary
|
2026-06-02 16:22:59 +08:00
|
|
|
|
const relatedApplication = isApplicationDocument ? null : resolveRelatedApplicationInfo(claim, typeLabel)
|
2026-06-03 15:46:56 +08:00
|
|
|
|
const expenseItems = buildExpenseItems(claim, riskMeta)
|
2026-06-03 17:31:40 +08:00
|
|
|
|
const visibleExpenseAmount = expenseItems.reduce((sum, item) => {
|
|
|
|
|
|
const amount = parseOptionalAmount(item.reimbursableAmount) ?? parseNumber(item.itemAmount)
|
|
|
|
|
|
return sum + amount
|
|
|
|
|
|
}, 0)
|
2026-06-02 16:22:59 +08:00
|
|
|
|
const amountValue = relatedApplication
|
|
|
|
|
|
? expenseItems.length
|
|
|
|
|
|
? visibleExpenseAmount
|
|
|
|
|
|
: invoiceCount === 0
|
|
|
|
|
|
? 0
|
|
|
|
|
|
: parseNumber(claim?.amount)
|
|
|
|
|
|
: parseNumber(claim?.amount)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const applyDateTime = claim?.submitted_at || claim?.created_at
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const employeeId = String(claim?.employee_id || claim?.employeeId || '').trim()
|
|
|
|
|
|
const employeeName = String(claim?.employee_name || claim?.employeeName || '').trim()
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: String(claim?.claim_no || claim?.id || '').trim(),
|
|
|
|
|
|
claimNo: String(claim?.claim_no || claim?.id || '').trim(),
|
|
|
|
|
|
claimId: String(claim?.id || '').trim(),
|
|
|
|
|
|
status: String(claim?.status || '').trim(),
|
2026-06-01 17:07:14 +08:00
|
|
|
|
employeeId,
|
|
|
|
|
|
employee_id: employeeId,
|
|
|
|
|
|
profileEmployeeId: employeeId || employeeName,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
person: String(claim?.employee_name || '').trim() || '待补充',
|
|
|
|
|
|
dept: String(claim?.department_name || '').trim() || '待补充',
|
|
|
|
|
|
departmentName: String(claim?.department_name || '').trim() || '待补充',
|
|
|
|
|
|
employeeName: String(claim?.employee_name || '').trim() || '待补充',
|
2026-05-13 06:55:23 +00:00
|
|
|
|
employeePosition: String(claim?.employee_position || '').trim(),
|
|
|
|
|
|
employeeGrade: String(claim?.employee_grade || '').trim(),
|
2026-05-21 09:28:33 +08:00
|
|
|
|
managerName: resolveDisplayName(claim?.manager_name),
|
2026-06-09 08:32:00 +00:00
|
|
|
|
financeApproverName: resolveDisplayName(claim?.finance_approver_name, claim?.financeApproverName),
|
|
|
|
|
|
financeOwnerName: resolveDisplayName(claim?.finance_owner_name, claim?.financeOwnerName),
|
2026-06-01 17:07:14 +08:00
|
|
|
|
budgetApproverName: resolveDisplayName(claim?.budget_approver_name, claim?.budgetApproverName),
|
|
|
|
|
|
budgetApproverGrade: String(claim?.budget_approver_grade || claim?.budgetApproverGrade || '').trim(),
|
|
|
|
|
|
budgetApproverRoleCode: String(claim?.budget_approver_role_code || claim?.budgetApproverRoleCode || '').trim(),
|
2026-05-13 06:55:23 +00:00
|
|
|
|
roleLabels: Array.isArray(claim?.role_labels) ? claim.role_labels.filter(Boolean) : [],
|
2026-05-13 03:29:10 +00:00
|
|
|
|
entity: '',
|
|
|
|
|
|
typeCode,
|
|
|
|
|
|
typeLabel,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
...documentTypeMeta,
|
|
|
|
|
|
detailVariant: typeCode === 'travel' || typeCode === 'travel_application' ? 'travel' : 'general',
|
|
|
|
|
|
title: String(claim?.reason || '').trim() || (isApplicationDocument ? typeLabel : `${typeLabel}报销`),
|
2026-05-13 03:29:10 +00:00
|
|
|
|
sceneLabel: typeLabel,
|
|
|
|
|
|
sceneTarget: String(claim?.location || '').trim() || '待补充',
|
|
|
|
|
|
location: String(claim?.location || '').trim() || '待补充',
|
|
|
|
|
|
relatedCustomer: '',
|
|
|
|
|
|
occurredDisplay: buildOccurredDisplay(claim),
|
|
|
|
|
|
occurredAt: claim?.occurred_at || '',
|
|
|
|
|
|
applyTime: formatDateTime(applyDateTime) || '待补充',
|
|
|
|
|
|
submittedAt: applyDateTime || '',
|
|
|
|
|
|
createdAt: claim?.created_at || '',
|
2026-05-22 16:00:19 +08:00
|
|
|
|
updatedAt: claim?.updated_at || '',
|
2026-06-02 16:22:59 +08:00
|
|
|
|
amount: amountValue,
|
2026-05-15 06:56:51 +00:00
|
|
|
|
riskFlags: Array.isArray(claim?.risk_flags_json) ? claim.risk_flags_json : [],
|
2026-06-03 15:46:56 +08:00
|
|
|
|
riskTone: riskMeta.tone,
|
|
|
|
|
|
riskLabel: riskMeta.label,
|
2026-05-15 06:56:51 +00:00
|
|
|
|
invoiceCount,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
workflowNode,
|
|
|
|
|
|
approvalKey: approvalMeta.key,
|
|
|
|
|
|
approvalStatus: approvalMeta.label,
|
|
|
|
|
|
approvalTone: approvalMeta.tone,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
secondaryStatusLabel: isApplicationDocument ? '申请材料' : (typeCode === 'travel' ? '行程状态' : '票据状态'),
|
|
|
|
|
|
secondaryStatusValue: isApplicationDocument
|
2026-05-27 14:35:17 +08:00
|
|
|
|
? approvalMeta.key === 'supplement'
|
|
|
|
|
|
? '领导已退回,待重新提交'
|
2026-06-06 17:19:07 +08:00
|
|
|
|
: applicationArchived
|
|
|
|
|
|
? '已归档'
|
|
|
|
|
|
: approvalMeta.key === 'completed'
|
|
|
|
|
|
? applicationLinkStatusText
|
|
|
|
|
|
: '已进入审批流程'
|
2026-05-25 13:35:39 +08:00
|
|
|
|
: (invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据'),
|
2026-05-27 14:35:17 +08:00
|
|
|
|
secondaryStatusTone: isApplicationDocument
|
2026-06-06 17:19:07 +08:00
|
|
|
|
? approvalMeta.key === 'supplement'
|
|
|
|
|
|
? 'warning'
|
|
|
|
|
|
: approvalMeta.key === 'completed' && !applicationArchived && !applicationLinkedReimbursementNo
|
|
|
|
|
|
? 'warning'
|
|
|
|
|
|
: 'success'
|
2026-05-27 14:35:17 +08:00
|
|
|
|
: (invoiceCount > 0 ? 'success' : 'warning'),
|
2026-05-13 03:29:10 +00:00
|
|
|
|
riskSummary,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
attachmentSummary: isApplicationDocument ? '申请单' : (invoiceCount > 0 ? `${invoiceCount} 张票据` : '无'),
|
|
|
|
|
|
expenseTableSummary: isApplicationDocument
|
2026-05-26 17:29:35 +08:00
|
|
|
|
? '预计金额已随申请提交'
|
2026-05-25 13:35:39 +08:00
|
|
|
|
: expenseItems.length
|
2026-05-13 03:29:10 +00:00
|
|
|
|
? (invoiceCount > 0
|
|
|
|
|
|
? `共 ${expenseItems.length} 条费用明细,已关联 ${invoiceCount} 张票据`
|
|
|
|
|
|
: `共 ${expenseItems.length} 条费用明细,待补充票据`)
|
|
|
|
|
|
: '暂无费用明细',
|
|
|
|
|
|
note: String(claim?.reason || '').trim(),
|
2026-05-30 15:46:51 +08:00
|
|
|
|
relatedApplication,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
progressSteps: buildProgressSteps(approvalMeta, workflowNode, claim, {
|
|
|
|
|
|
documentTypeCode: documentTypeMeta.documentTypeCode
|
|
|
|
|
|
}),
|
2026-05-13 03:29:10 +00:00
|
|
|
|
expenseItems
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getWeekStart(date) {
|
|
|
|
|
|
const nextDate = new Date(date)
|
|
|
|
|
|
const day = nextDate.getDay() || 7
|
|
|
|
|
|
nextDate.setHours(0, 0, 0, 0)
|
|
|
|
|
|
nextDate.setDate(nextDate.getDate() - day + 1)
|
|
|
|
|
|
return nextDate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 15:38:59 +00:00
|
|
|
|
function getRecentDaysStart(date, days) {
|
|
|
|
|
|
const nextDate = new Date(date)
|
|
|
|
|
|
nextDate.setHours(0, 0, 0, 0)
|
|
|
|
|
|
nextDate.setDate(nextDate.getDate() - Math.max(0, Number(days || 1) - 1))
|
|
|
|
|
|
return nextDate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
function resolveRangeMatch(activeRange, item) {
|
|
|
|
|
|
if (activeRange === 'custom' || activeRange === '本月') {
|
|
|
|
|
|
if (activeRange !== '本月') {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const targetDate = toDate(item?.submittedAt || item?.createdAt || item?.occurredAt)
|
|
|
|
|
|
if (!targetDate) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const targetDay = formatDate(targetDate)
|
|
|
|
|
|
|
|
|
|
|
|
if (activeRange === '今日') {
|
|
|
|
|
|
return targetDay === formatDate(now)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 15:38:59 +00:00
|
|
|
|
if (activeRange === '近10日') {
|
|
|
|
|
|
const recentStart = getRecentDaysStart(now, 10)
|
|
|
|
|
|
return targetDate >= recentStart && targetDate <= now
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
|
if (activeRange === '本周') {
|
|
|
|
|
|
const weekStart = getWeekStart(now)
|
|
|
|
|
|
const nextWeekStart = new Date(weekStart)
|
|
|
|
|
|
nextWeekStart.setDate(nextWeekStart.getDate() + 7)
|
|
|
|
|
|
return targetDate >= weekStart && targetDate < nextWeekStart
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (activeRange === '本月') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
targetDate.getFullYear() === now.getFullYear()
|
|
|
|
|
|
&& targetDate.getMonth() === now.getMonth()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useRequests() {
|
|
|
|
|
|
const requests = ref([])
|
|
|
|
|
|
const loading = ref(false)
|
2026-05-29 13:17:39 +08:00
|
|
|
|
const loaded = ref(false)
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const error = ref('')
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
const search = ref('')
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' })
|
2026-05-13 15:38:59 +00:00
|
|
|
|
const ranges = ['今日', '近10日', '本周', '本月']
|
|
|
|
|
|
const activeRange = ref('近10日')
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
|
|
|
|
|
|
const filteredRequests = computed(() => {
|
|
|
|
|
|
const key = search.value.trim().toLowerCase()
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
return requests.value.filter((item) => {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
const searchText = [
|
|
|
|
|
|
item.id,
|
|
|
|
|
|
item.person,
|
|
|
|
|
|
item.typeLabel,
|
|
|
|
|
|
item.title,
|
|
|
|
|
|
item.sceneTarget,
|
|
|
|
|
|
item.riskSummary
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join('')
|
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
|
|
|
|
|
|
const matchesSearch = !key || searchText.includes(key)
|
|
|
|
|
|
const matchesRange = resolveRangeMatch(activeRange.value, item)
|
|
|
|
|
|
|
|
|
|
|
|
return matchesSearch && matchesRange
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-09 08:32:00 +00:00
|
|
|
|
async function reload(options = {}) {
|
|
|
|
|
|
const silent = Boolean(options?.silent)
|
|
|
|
|
|
if (!silent) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
const payload = await fetchAllExpenseClaims()
|
2026-05-13 03:29:10 +00:00
|
|
|
|
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
|
2026-05-29 13:17:39 +08:00
|
|
|
|
loaded.value = true
|
2026-05-13 03:29:10 +00:00
|
|
|
|
} catch (nextError) {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
if (!silent) {
|
|
|
|
|
|
requests.value = []
|
|
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
|
|
|
|
|
|
} finally {
|
2026-06-09 08:32:00 +00:00
|
|
|
|
if (!silent) {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
}
|
2026-04-29 23:35:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
function approveRequest(request) {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rejectRequest(request) {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
|
function ensureLoaded() {
|
|
|
|
|
|
return loaded.value ? Promise.resolve() : reload()
|
|
|
|
|
|
}
|
2026-05-13 03:29:10 +00:00
|
|
|
|
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
return {
|
2026-05-13 03:29:10 +00:00
|
|
|
|
requests,
|
|
|
|
|
|
loading,
|
2026-05-29 13:17:39 +08:00
|
|
|
|
loaded,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
error,
|
|
|
|
|
|
search,
|
|
|
|
|
|
filters,
|
|
|
|
|
|
ranges,
|
|
|
|
|
|
activeRange,
|
|
|
|
|
|
filteredRequests,
|
|
|
|
|
|
approveRequest,
|
|
|
|
|
|
rejectRequest,
|
2026-05-29 13:17:39 +08:00
|
|
|
|
ensureLoaded,
|
2026-05-13 03:29:10 +00:00
|
|
|
|
reload
|
feat: refactor monolithic App.vue into modular Vue component architecture
- Extract 711-line App.vue into 15+ focused files across 5 directories
- Add data layer (icons, metrics, policies, auditTrail, requests)
- Add composables (useNavigation, useRequests, useChat, useToast)
- Add layout components (SidebarRail, TopBar, FilterBar)
- Add shared components (PanelHead, InfoRow, ToastNotification)
- Add business component (RequestTable) and 5 view components
- Extract global CSS to assets/styles/global.css
- Add start.sh with WSL/Windows cross-platform support
- Add .gitignore for node_modules, dist, and IDE dirs
2026-04-28 17:20:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|