2026-05-13 03:31:07 +00:00
|
|
|
|
const REQUEST_TYPE_META = {
|
|
|
|
|
|
travel: {
|
|
|
|
|
|
label: '差旅费',
|
|
|
|
|
|
detailVariant: 'travel',
|
|
|
|
|
|
tone: 'travel',
|
|
|
|
|
|
secondaryStatusLabel: '行程状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
entertainment: {
|
|
|
|
|
|
label: '业务招待费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'entertainment',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
hotel: {
|
|
|
|
|
|
label: '住宿费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'travel',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
transport: {
|
|
|
|
|
|
label: '交通费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'travel',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
meal: {
|
|
|
|
|
|
label: '餐费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'meeting',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
office: {
|
|
|
|
|
|
label: '办公费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'office',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
meeting: {
|
|
|
|
|
|
label: '会务费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'meeting',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
training: {
|
|
|
|
|
|
label: '培训费',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'training',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
},
|
|
|
|
|
|
other: {
|
|
|
|
|
|
label: '其他费用',
|
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
|
tone: 'other',
|
|
|
|
|
|
secondaryStatusLabel: '票据状态'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const APPROVAL_META = {
|
|
|
|
|
|
draft: { label: '草稿', tone: 'draft' },
|
|
|
|
|
|
in_progress: { label: '审批中', tone: 'info' },
|
|
|
|
|
|
supplement: { label: '待补充', tone: 'warning' },
|
|
|
|
|
|
completed: { label: '已完成', tone: 'success' },
|
|
|
|
|
|
rejected: { label: '已退回', tone: 'danger' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const BACKEND_STATUS_META = {
|
|
|
|
|
|
draft: { key: 'draft', label: '草稿', tone: 'draft' },
|
|
|
|
|
|
submitted: { key: 'in_progress', label: '审批中', tone: 'info' },
|
|
|
|
|
|
pending: { key: 'in_progress', label: '审批中', tone: 'info' },
|
|
|
|
|
|
reviewing: { key: 'in_progress', label: '审批中', tone: 'info' },
|
|
|
|
|
|
in_review: { key: 'in_progress', label: '审批中', tone: 'info' },
|
|
|
|
|
|
in_progress: { key: 'in_progress', label: '审批中', tone: 'info' },
|
|
|
|
|
|
approved: { key: 'completed', label: '已完成', tone: 'success' },
|
|
|
|
|
|
paid: { key: 'completed', label: '已完成', tone: 'success' },
|
|
|
|
|
|
completed: { key: 'completed', label: '已完成', tone: 'success' },
|
|
|
|
|
|
supplement: { key: 'supplement', label: '待补充', tone: 'warning' },
|
2026-05-20 14:32:35 +08:00
|
|
|
|
returned: { key: 'supplement', label: '待提交', tone: 'warning' },
|
2026-05-13 03:31:07 +00:00
|
|
|
|
rejected: { key: 'rejected', label: '已退回', tone: 'danger' },
|
|
|
|
|
|
cancelled: { key: 'rejected', label: '已退回', tone: 'danger' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 22:23:42 +08:00
|
|
|
|
function parseRequestDateFromId(id) {
|
2026-05-13 03:31:07 +00:00
|
|
|
|
const match = String(id || '').match(/(\d{4})[-]?(\d{2})(\d{2})/)
|
2026-05-06 22:23:42 +08:00
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const [, year, month, day] = match
|
|
|
|
|
|
return `${year}-${month}-${day}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
function parseAmount(value) {
|
|
|
|
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
|
|
|
|
return value
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
const normalized = String(value || '')
|
|
|
|
|
|
.replace(/[,,\s]/g, '')
|
|
|
|
|
|
.replace(/[¥¥]/g, '')
|
|
|
|
|
|
.replace(/元/g, '')
|
|
|
|
|
|
.trim()
|
2026-05-06 22:23:42 +08:00
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
if (!normalized || !/^-?\d+(?:\.\d+)?$/.test(normalized)) {
|
|
|
|
|
|
return null
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
const amount = Number(normalized)
|
|
|
|
|
|
return Number.isFinite(amount) ? amount : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatRequestCurrency(value) {
|
|
|
|
|
|
const amount = parseAmount(value)
|
|
|
|
|
|
if (amount === null) {
|
|
|
|
|
|
return String(value || '').trim() || '待补充'
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
return new Intl.NumberFormat('zh-CN', {
|
|
|
|
|
|
style: 'currency',
|
|
|
|
|
|
currency: 'CNY',
|
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
|
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
|
|
|
|
|
|
}).format(amount)
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
function resolveApprovalState(request) {
|
|
|
|
|
|
const normalizedStatus = String(request?.status || '').trim().toLowerCase()
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedStatus && BACKEND_STATUS_META[normalizedStatus]) {
|
|
|
|
|
|
const meta = BACKEND_STATUS_META[normalizedStatus]
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: meta.key,
|
|
|
|
|
|
label: request.approvalStatus || request.approval || meta.label,
|
|
|
|
|
|
tone: request.approvalTone || meta.tone
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (request?.approvalKey && APPROVAL_META[request.approvalKey]) {
|
2026-05-06 22:23:42 +08:00
|
|
|
|
return {
|
2026-05-13 03:31:07 +00:00
|
|
|
|
key: request.approvalKey,
|
|
|
|
|
|
label: request.approvalStatus || request.approval || APPROVAL_META[request.approvalKey].label,
|
|
|
|
|
|
tone: request.approvalTone || APPROVAL_META[request.approvalKey].tone
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
if (typeof request?.approval === 'string' && request.approval.trim()) {
|
2026-05-06 22:23:42 +08:00
|
|
|
|
return {
|
2026-05-13 03:31:07 +00:00
|
|
|
|
key: request.approvalKey || 'in_progress',
|
|
|
|
|
|
label: request.approval,
|
|
|
|
|
|
tone: request.approvalTone || 'info'
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
if (typeof request?.status === 'string') {
|
|
|
|
|
|
if (request.status === 'success') return { key: 'completed', label: '已完成', tone: 'success' }
|
|
|
|
|
|
if (request.status === 'danger') return { key: 'supplement', label: '待补充', tone: 'warning' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { key: 'in_progress', label: '审批中', tone: 'info' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveTypeMeta(request) {
|
|
|
|
|
|
const typeCode = String(request?.typeCode || request?.expense_type || '').trim() || 'other'
|
|
|
|
|
|
const typeMeta = REQUEST_TYPE_META[typeCode] || REQUEST_TYPE_META.other
|
|
|
|
|
|
|
2026-05-06 22:23:42 +08:00
|
|
|
|
return {
|
2026-05-13 03:31:07 +00:00
|
|
|
|
typeCode,
|
|
|
|
|
|
typeLabel: String(request?.typeLabel || request?.category || '').trim() || typeMeta.label,
|
|
|
|
|
|
detailVariant: String(request?.detailVariant || '').trim() || typeMeta.detailVariant,
|
|
|
|
|
|
typeTone: typeMeta.tone,
|
|
|
|
|
|
secondaryStatusLabel: request?.secondaryStatusLabel || typeMeta.secondaryStatusLabel
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:55:23 +00:00
|
|
|
|
function normalizeRoleLabels(value) {
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
|
return value.map((item) => String(item || '').trim()).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const text = String(value || '').trim()
|
|
|
|
|
|
return text ? [text] : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 22:23:42 +08:00
|
|
|
|
export function normalizeRequestForUi(request) {
|
|
|
|
|
|
if (!request) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 03:31:07 +00:00
|
|
|
|
const { typeCode, typeLabel, detailVariant, typeTone, secondaryStatusLabel } = resolveTypeMeta(request)
|
|
|
|
|
|
const approvalState = resolveApprovalState(request)
|
|
|
|
|
|
const amountValue = parseAmount(request.amount)
|
|
|
|
|
|
const amountDisplay = formatRequestCurrency(amountValue ?? request.amount)
|
|
|
|
|
|
const title = String(request.title || request.reason || '').trim() || `${typeLabel}报销`
|
|
|
|
|
|
const sceneTarget = String(request.sceneTarget || request.location || request.city || request.entity || '').trim() || '待补充'
|
|
|
|
|
|
const occurredDisplay = String(request.occurredDisplay || request.period || request.occurredAt || '').trim() || '待补充'
|
|
|
|
|
|
const applyTime = String(request.applyTime || parseRequestDateFromId(request.id) || '').trim() || '待补充'
|
|
|
|
|
|
const workflowNode = String(request.workflowNode || request.node || '').trim() || '待提交'
|
|
|
|
|
|
const secondaryStatusValue =
|
|
|
|
|
|
String(request.secondaryStatusValue || request.travel || '').trim()
|
|
|
|
|
|
|| (detailVariant === 'travel' ? '待安排行程' : '待补充票据')
|
|
|
|
|
|
const secondaryStatusTone = String(request.secondaryStatusTone || request.travelTone || '').trim() || 'neutral'
|
2026-05-13 06:55:23 +00:00
|
|
|
|
const roleLabels = normalizeRoleLabels(request.roleLabels || request.role_labels)
|
|
|
|
|
|
const profileIdentity =
|
|
|
|
|
|
String(request.profileIdentity || request.employeeIdentity || request.identity || '').trim()
|
|
|
|
|
|
|| roleLabels.join(' / ')
|
|
|
|
|
|
|| '员工'
|
2026-05-06 22:23:42 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...request,
|
2026-05-13 03:31:07 +00:00
|
|
|
|
claimId: String(request.claimId || request.claim_id || '').trim(),
|
|
|
|
|
|
documentNo: String(request.documentNo || request.claimNo || request.claim_no || request.id || '').trim(),
|
|
|
|
|
|
typeCode,
|
|
|
|
|
|
typeLabel,
|
|
|
|
|
|
detailVariant,
|
|
|
|
|
|
typeTone,
|
|
|
|
|
|
title,
|
|
|
|
|
|
reason: title,
|
|
|
|
|
|
sceneLabel: String(request.sceneLabel || '').trim() || typeLabel,
|
|
|
|
|
|
sceneTarget,
|
|
|
|
|
|
city: sceneTarget,
|
|
|
|
|
|
location: String(request.location || '').trim() || sceneTarget,
|
|
|
|
|
|
relatedCustomer: String(request.relatedCustomer || '').trim() || '待补充',
|
|
|
|
|
|
occurredDisplay,
|
|
|
|
|
|
period: occurredDisplay,
|
2026-05-06 22:23:42 +08:00
|
|
|
|
applyTime,
|
2026-05-13 03:31:07 +00:00
|
|
|
|
amountValue,
|
|
|
|
|
|
amountDisplay,
|
|
|
|
|
|
amount: amountDisplay,
|
|
|
|
|
|
workflowNode,
|
|
|
|
|
|
node: workflowNode,
|
|
|
|
|
|
approvalKey: approvalState.key,
|
|
|
|
|
|
approvalStatus: approvalState.label,
|
|
|
|
|
|
approval: approvalState.label,
|
|
|
|
|
|
approvalTone: approvalState.tone,
|
|
|
|
|
|
secondaryStatusLabel,
|
|
|
|
|
|
secondaryStatusValue,
|
|
|
|
|
|
secondaryStatusTone,
|
|
|
|
|
|
travel: secondaryStatusValue,
|
|
|
|
|
|
travelTone: secondaryStatusTone,
|
|
|
|
|
|
riskSummary: String(request.riskSummary || request.risk || '').trim() || '暂无异常',
|
|
|
|
|
|
attachmentSummary: String(request.attachmentSummary || '').trim() || '待补充',
|
|
|
|
|
|
rangeBucket: String(request.rangeBucket || request.range || '').trim() || '本周',
|
|
|
|
|
|
detailTitle: String(request.detailTitle || '').trim() || title,
|
|
|
|
|
|
note: String(request.note || '').trim(),
|
|
|
|
|
|
profileName: String(request.person || request.applicant || request.employeeName || '').trim() || '当前申请人',
|
|
|
|
|
|
profileDepartment: String(request.dept || request.department || request.departmentName || '').trim() || '所属部门',
|
2026-05-13 06:55:23 +00:00
|
|
|
|
profileIdentity,
|
|
|
|
|
|
profilePosition:
|
|
|
|
|
|
String(request.profilePosition || request.employeePosition || request.employee_position || request.position || '').trim()
|
|
|
|
|
|
|| '待补充',
|
|
|
|
|
|
profileGrade: String(request.profileGrade || request.employeeGrade || request.employee_grade || request.grade || '').trim() || '待补充',
|
|
|
|
|
|
profileManager: String(request.profileManager || request.managerName || request.manager_name || request.manager || '').trim() || '待补充',
|
|
|
|
|
|
roleLabels,
|
2026-05-13 03:31:07 +00:00
|
|
|
|
profileAvatar:
|
|
|
|
|
|
String(request.person || request.applicant || request.employeeName || '申').trim().slice(0, 1) || '申'
|
2026-05-06 22:23:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|