feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
@@ -30,6 +30,92 @@ function pickDetailValue(detail, request, keys = [], fallback = '') {
|
||||
return normalizeText(fallback)
|
||||
}
|
||||
|
||||
function isTravelApplicationDetail(detail = {}, request = {}) {
|
||||
const typeText = [
|
||||
detail.application_type,
|
||||
detail.applicationType,
|
||||
request.typeCode,
|
||||
request.typeLabel,
|
||||
request.documentTypeLabel
|
||||
].map(normalizeText).join(' ')
|
||||
return /travel_application|差旅|出差/.test(typeText)
|
||||
}
|
||||
|
||||
function isEntertainmentApplicationDetail(detail = {}, request = {}) {
|
||||
const typeText = [
|
||||
detail.application_type,
|
||||
detail.applicationType,
|
||||
request.typeCode,
|
||||
request.typeLabel
|
||||
].map(normalizeText).join(' ')
|
||||
return /entertainment|招待/.test(typeText)
|
||||
}
|
||||
|
||||
function extractDateRange(value) {
|
||||
const dates = normalizeText(value).match(/\d{4}-\d{2}-\d{2}/g) || []
|
||||
return {
|
||||
startDate: dates[0] || '',
|
||||
endDate: dates[dates.length - 1] || ''
|
||||
}
|
||||
}
|
||||
|
||||
function extractDayCount(value) {
|
||||
const match = normalizeText(value).replace(/\s+/g, '').match(/(\d{1,2})天/)
|
||||
return match ? Number(match[1]) || 0 : 0
|
||||
}
|
||||
|
||||
function addDays(dateText, days) {
|
||||
if (!dateText || days <= 1) {
|
||||
return dateText
|
||||
}
|
||||
const date = new Date(`${dateText}T00:00:00`)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return dateText
|
||||
}
|
||||
date.setDate(date.getDate() + days - 1)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function buildApplicationTimeRows(detail, request) {
|
||||
const timeValue = pickDetailValue(detail, request, ['time', 'occurredDisplay', 'period'], request.occurredDisplay)
|
||||
if (!isProvided(timeValue)) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (isTravelApplicationDetail(detail, request)) {
|
||||
const days = extractDayCount(pickDetailValue(detail, request, ['days']))
|
||||
const range = extractDateRange(timeValue)
|
||||
const startDate = range.startDate || timeValue
|
||||
const endDate = range.endDate && range.endDate !== range.startDate
|
||||
? range.endDate
|
||||
: addDays(range.startDate, days)
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'trip_start_time',
|
||||
label: '出发时间',
|
||||
value: startDate
|
||||
},
|
||||
{
|
||||
key: 'trip_return_time',
|
||||
label: '返回时间',
|
||||
value: endDate
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'time',
|
||||
label: isEntertainmentApplicationDetail(detail, request) ? '招待时间' : '申请时间',
|
||||
value: timeValue
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildApplicationDetailFactItems(request = {}) {
|
||||
const detail = resolveApplicationDetailPayload(request)
|
||||
const amountDisplay = normalizeText(request.amountDisplay || request.amount)
|
||||
@@ -39,11 +125,7 @@ export function buildApplicationDetailFactItems(request = {}) {
|
||||
label: '申请类型',
|
||||
value: pickDetailValue(detail, request, ['application_type', 'typeLabel'], request.typeLabel)
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
label: '发生时间',
|
||||
value: pickDetailValue(detail, request, ['time', 'occurredDisplay', 'period'], request.occurredDisplay)
|
||||
},
|
||||
...buildApplicationTimeRows(detail, request),
|
||||
{
|
||||
key: 'location',
|
||||
label: '地点',
|
||||
@@ -107,6 +189,12 @@ export function buildApplicationDetailFactItems(request = {}) {
|
||||
|
||||
export function buildRelatedApplicationFactItems(request = {}) {
|
||||
const related = request.relatedApplication || {}
|
||||
const relatedRange = extractDateRange(related.time)
|
||||
const relatedStartDate = normalizeText(related.tripStartDate) || relatedRange.startDate
|
||||
const relatedEndDate = normalizeText(related.tripEndDate) || relatedRange.endDate || addDays(
|
||||
relatedStartDate,
|
||||
extractDayCount(related.days)
|
||||
)
|
||||
const rows = [
|
||||
{
|
||||
key: 'claim_no',
|
||||
@@ -119,6 +207,16 @@ export function buildRelatedApplicationFactItems(request = {}) {
|
||||
label: '申请内容',
|
||||
value: related.content
|
||||
},
|
||||
{
|
||||
key: 'trip_start_date',
|
||||
label: '出发时间',
|
||||
value: relatedStartDate
|
||||
},
|
||||
{
|
||||
key: 'trip_end_date',
|
||||
label: '返回时间',
|
||||
value: relatedEndDate
|
||||
},
|
||||
{
|
||||
key: 'days',
|
||||
label: '申请天数',
|
||||
@@ -135,9 +233,37 @@ export function buildRelatedApplicationFactItems(request = {}) {
|
||||
value: related.location
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
label: '申请时间',
|
||||
value: related.time
|
||||
key: 'transport_mode',
|
||||
label: '出行方式',
|
||||
value: related.transportMode
|
||||
},
|
||||
{
|
||||
key: 'lodging_daily_cap',
|
||||
label: '住宿上限/天',
|
||||
value: related.lodgingDailyCap,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
key: 'subsidy_daily_cap',
|
||||
label: '补贴标准/天',
|
||||
value: related.subsidyDailyCap,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
key: 'transport_policy',
|
||||
label: '交通费用口径',
|
||||
value: related.transportPolicy
|
||||
},
|
||||
{
|
||||
key: 'policy_estimate',
|
||||
label: '规则测算参考',
|
||||
value: related.policyEstimate,
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
key: 'rule',
|
||||
label: '规则依据',
|
||||
value: related.ruleLabel
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
@@ -145,11 +271,6 @@ export function buildRelatedApplicationFactItems(request = {}) {
|
||||
value: related.amountLabel,
|
||||
highlight: true,
|
||||
emphasis: true
|
||||
},
|
||||
{
|
||||
key: 'transport_mode',
|
||||
label: '出行方式',
|
||||
value: related.transportMode
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export function buildMockApplicationTransportEstimate({
|
||||
simulatedLatencyMs,
|
||||
source: 'mock_ticket_price_query_v1',
|
||||
confidence: 'mock',
|
||||
basisText: `已查询 ${queryLabel} ${mode}参考票价,按${bandLabel}往返 ${amountDisplay}元估算(查询耗时 ${simulatedLatencyMs}ms)`
|
||||
basisText: `预估交通费用 ${amountDisplay}元`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,19 @@ export function resolveExpenseTypeCode(ontology) {
|
||||
return String(entity?.normalized_value || entity?.value || 'other').trim() || 'other'
|
||||
}
|
||||
|
||||
function looksLikeStructuredTravelApplication(prompt) {
|
||||
const text = String(prompt || '')
|
||||
return /(?:发生时间|业务发生时间|申请时间|时间)\s*[::]/.test(text)
|
||||
&& /(?:地点|业务地点|发生地点|目的地)\s*[::]/.test(text)
|
||||
&& /(?:天数|出差天数|申请天数)\s*[::]?\s*(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/.test(text)
|
||||
}
|
||||
|
||||
function resolveApplicationExpenseTypeCode(ontology, prompt) {
|
||||
const code = resolveExpenseTypeCode(ontology)
|
||||
if (code !== 'other') return code
|
||||
return looksLikeStructuredTravelApplication(prompt) ? 'travel' : code
|
||||
}
|
||||
|
||||
export function resolveExpenseTypeLabel(code) {
|
||||
return EXPENSE_TYPE_LABELS[String(code || '').trim()] || EXPENSE_TYPE_LABELS.other
|
||||
}
|
||||
@@ -358,7 +371,7 @@ export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
|
||||
}
|
||||
|
||||
export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser = {}) {
|
||||
const expenseTypeCode = resolveExpenseTypeCode(ontology)
|
||||
const expenseTypeCode = resolveApplicationExpenseTypeCode(ontology, prompt)
|
||||
const amount = resolveApplicationAmount(ontology)
|
||||
const documentTypeEntity = resolveEntity(ontology, 'document_type')
|
||||
const workflowStageEntity = resolveEntity(ontology, 'workflow_stage')
|
||||
|
||||
@@ -31,11 +31,11 @@ export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
|
||||
export const APPLICATION_TRANSPORT_MODE_OPTIONS = ['火车', '飞机', '轮船']
|
||||
|
||||
const APPLICATION_POLICY_PENDING_TEXT = '填写地点和天数后自动测算'
|
||||
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '选择火车、飞机或轮船后自动生成交通参考票价,报销阶段按真实票据复核'
|
||||
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '选择火车、飞机或轮船后自动预估交通费用'
|
||||
|
||||
export function resolveApplicationTimeLabel(applicationType = '') {
|
||||
const label = String(applicationType || '').trim()
|
||||
if (/差旅|出差/.test(label)) return '行程时间'
|
||||
if (/差旅|出差/.test(label)) return '出发时间'
|
||||
if (/招待|宴请|餐饮/.test(label)) return '招待时间'
|
||||
return '申请时间'
|
||||
}
|
||||
@@ -47,10 +47,36 @@ function resolveApplicationFieldLabel(item, fields = {}) {
|
||||
return item.label
|
||||
}
|
||||
|
||||
function isTravelApplicationType(applicationType = '') {
|
||||
return /差旅|出差/.test(String(applicationType || '').trim())
|
||||
}
|
||||
|
||||
function resolveApplicationTripDateParts(fields = {}) {
|
||||
const timeText = String(fields.time || '').trim()
|
||||
const matchedDates = timeText.match(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g) || []
|
||||
const startDate = normalizeDateText(matchedDates[0] || timeText)
|
||||
const explicitEndDate = normalizeDateText(matchedDates[matchedDates.length - 1] || '')
|
||||
const inferredEndDate = explicitEndDate && explicitEndDate !== startDate
|
||||
? explicitEndDate
|
||||
: buildEndDateFromDays(startDate, fields.days)
|
||||
|
||||
return {
|
||||
startDate,
|
||||
endDate: inferredEndDate || explicitEndDate || startDate
|
||||
}
|
||||
}
|
||||
|
||||
function compactText(value) {
|
||||
return String(value || '').replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function looksLikeStructuredTravelApplication(text) {
|
||||
const source = String(text || '')
|
||||
return /(?:发生时间|业务发生时间|申请时间|时间)\s*[::]/.test(source)
|
||||
&& /(?:地点|业务地点|发生地点|目的地)\s*[::]/.test(source)
|
||||
&& /(?:天数|出差天数|申请天数)\s*[::]?\s*(?:\d+|[一二两三四五六七八九十]{1,3})\s*天/.test(source)
|
||||
}
|
||||
|
||||
function resolveFirstMatch(text, patterns = []) {
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern)
|
||||
@@ -106,6 +132,7 @@ function resolvePreviewToday(options = {}) {
|
||||
|
||||
function resolveApplicationType(text) {
|
||||
const compact = compactText(text)
|
||||
if (looksLikeStructuredTravelApplication(text)) return '差旅费用申请'
|
||||
if (/差旅|出差|高铁|动车|火车|飞机|机票|航班|酒店|住宿/.test(compact)) return '差旅费用申请'
|
||||
if (/交通|出租车|的士|网约车|打车|通勤/.test(compact)) return '交通费用申请'
|
||||
if (/住宿|酒店/.test(compact)) return '住宿费用申请'
|
||||
@@ -224,7 +251,7 @@ function buildTransportPolicyText(transportMode, location = '', transportEstimat
|
||||
if (!mode) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
|
||||
const estimate = transportEstimate || buildMockApplicationTransportEstimate({ transportMode: mode, location, time })
|
||||
if (!estimate) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
|
||||
return `${estimate.basisText},报销阶段按真实票据复核`
|
||||
return estimate.basisText
|
||||
}
|
||||
|
||||
function ensureApplicationPolicyFields(fields = {}) {
|
||||
@@ -437,9 +464,8 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
|
||||
allowanceAmount: result?.allowance_amount
|
||||
})
|
||||
const transportEstimate = systemEstimate.transportEstimate
|
||||
const queryLabel = transportEstimate?.queryDate || '出行日期待确认'
|
||||
const transportText = transportEstimate
|
||||
? `交通 ${systemEstimate.transportAmountDisplay}元(按 ${queryLabel} 参考票价) + `
|
||||
? `交通 ${systemEstimate.transportAmountDisplay}元 + `
|
||||
: ''
|
||||
const totalAmount = systemEstimate.totalAmountDisplay
|
||||
const amount = totalAmount ? `${totalAmount}元` : fields.amount
|
||||
@@ -499,7 +525,6 @@ export function refreshApplicationPreviewTransportEstimate(preview = {}) {
|
||||
const hotelAmount = formatPolicyMoney(hotelAmountSource)
|
||||
const allowanceAmount = formatPolicyMoney(allowanceAmountSource)
|
||||
const hasPolicyAmounts = parseMoneyNumber(hotelAmountSource) > 0 || parseMoneyNumber(allowanceAmountSource) > 0
|
||||
const queryLabel = transportEstimate.queryDate || '出行日期待确认'
|
||||
const nextFields = {
|
||||
...fields,
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode, location, transportEstimate, fields.time),
|
||||
@@ -513,7 +538,7 @@ export function refreshApplicationPreviewTransportEstimate(preview = {}) {
|
||||
if (hasPolicyAmounts) {
|
||||
const days = Number(policyResult.days) || parseApplicationDaysValue(fields.days) || 1
|
||||
const totalAmount = systemEstimate.totalAmountDisplay
|
||||
nextFields.policyEstimate = `交通 ${systemEstimate.transportAmountDisplay}元(按 ${queryLabel} 参考票价) + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`
|
||||
nextFields.policyEstimate = `交通 ${systemEstimate.transportAmountDisplay}元 + 住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天)`
|
||||
nextFields.amount = totalAmount ? `${totalAmount}元` : nextFields.amount
|
||||
nextFields.policyTotalAmount = totalAmount ? `${totalAmount}元` : ''
|
||||
}
|
||||
@@ -639,17 +664,41 @@ export function buildModelRefinedApplicationPreview(localPreview = {}, ontology
|
||||
export function buildApplicationPreviewRows(preview = {}) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
return APPLICATION_PREVIEW_FIELD_DEFINITIONS.map((item) => {
|
||||
return APPLICATION_PREVIEW_FIELD_DEFINITIONS.flatMap((item) => {
|
||||
if (item.key === 'time' && isTravelApplicationType(fields.applicationType)) {
|
||||
const tripDates = resolveApplicationTripDateParts(fields)
|
||||
const rawValue = fields[item.key]
|
||||
const missing = item.required !== false && !isApplicationPreviewValueProvided(rawValue)
|
||||
return [
|
||||
{
|
||||
...item,
|
||||
label: '出发时间',
|
||||
value: tripDates.startDate || '待补充',
|
||||
editable: item.editable !== false,
|
||||
highlight: Boolean(item.highlight),
|
||||
missing
|
||||
},
|
||||
{
|
||||
key: 'time_return',
|
||||
label: '返回时间',
|
||||
value: tripDates.endDate || '待补充',
|
||||
editable: false,
|
||||
highlight: Boolean(item.highlight),
|
||||
missing
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rawValue = fields[item.key]
|
||||
const value = String(rawValue || '').trim() || '待补充'
|
||||
return {
|
||||
return [{
|
||||
...item,
|
||||
label: resolveApplicationFieldLabel(item, fields),
|
||||
value,
|
||||
editable: item.editable !== false,
|
||||
highlight: Boolean(item.highlight),
|
||||
missing: item.required !== false && !isApplicationPreviewValueProvided(rawValue)
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user