Files
X-Financial/web/src/views/scripts/travelReimbursementExpenseQueryModel.js

307 lines
12 KiB
JavaScript
Raw Normal View History

import {
EXPENSE_TYPE_LABELS,
formatAmountDisplay
} from './travelReimbursementReviewModel.js'
export const EXPENSE_QUERY_PAGE_SIZE = 5
export const EXPENSE_CENTER_HREF = '/app/documents'
export const ASSOCIATABLE_CLAIM_STATUSES = new Set(['draft', 'supplement', 'returned'])
const EXPENSE_STATUS_LABELS = {
draft: '草稿',
supplement: '待补充',
returned: '已退回',
submitted: '已提交',
review: '审批中',
approved: '已审核',
pending_payment: '待付款',
paid: '已付款'
}
const EXPENSE_RISK_LEVEL_LABELS = {
high: '高风险',
medium: '中风险',
warning: '中风险',
low: '低风险',
info: '提示'
}
export function normalizeExpenseQueryRiskItem(item, index = 0) {
if (!item || typeof item !== 'object') {
return null
}
const rawLevel = String(item.level || item.severity || '').trim().toLowerCase()
const level = EXPENSE_RISK_LEVEL_LABELS[rawLevel] ? rawLevel : 'medium'
const summary = String(item.summary || item.message || item.content || '').trim()
const detail = String(item.detail || item.description || summary).trim()
if (!summary && !detail) {
return null
}
return {
key: String(item.key || `${level}-${index}`).trim() || `${level}-${index}`,
level,
levelLabel: String(item.level_label || item.levelLabel || EXPENSE_RISK_LEVEL_LABELS[level]).trim() || EXPENSE_RISK_LEVEL_LABELS[level],
title: String(item.title || item.label || EXPENSE_RISK_LEVEL_LABELS[level]).trim() || EXPENSE_RISK_LEVEL_LABELS[level],
summary: summary || detail,
detail: detail || summary
}
}
export function normalizeExpenseQueryStatusGroup(item) {
if (!item || typeof item !== 'object') {
return null
}
const rawCount = Number(item.count || 0)
return {
key: String(item.key || 'other').trim() || 'other',
label: String(item.label || '其他状态').trim() || '其他状态',
count: Number.isFinite(rawCount) ? Math.max(0, rawCount) : 0
}
}
export function normalizeExpenseQueryRecord(item) {
if (!item || typeof item !== 'object') {
return null
}
const amount = Number(item.amount || 0)
const amountValue = Number.isFinite(amount) ? amount : 0
const expenseTypeLabel = String(item.expense_type_label || item.expense_type || '报销').trim() || '报销'
const reason = String(item.reason || '').trim()
const documentDate = String(item.document_date || '').trim()
const occurredAt = String(item.occurred_at || '').trim()
return {
claimId: String(item.claim_id || '').trim(),
claimNo: String(item.claim_no || '').trim() || '未编号',
employeeName: String(item.employee_name || '').trim(),
expenseType: String(item.expense_type || '').trim(),
expenseTypeLabel,
amount: amountValue,
amountDisplay: formatAmountDisplay(amountValue),
status: String(item.status || '').trim(),
statusLabel: String(item.status_label || '处理中').trim() || '处理中',
statusGroup: String(item.status_group || 'other').trim() || 'other',
statusGroupLabel: String(item.status_group_label || '其他状态').trim() || '其他状态',
approvalStage: String(item.approval_stage || '').trim(),
documentDate,
occurredAt,
reason,
location: String(item.location || '').trim(),
riskItems: (Array.isArray(item.risk_flags) ? item.risk_flags : [])
.map((riskItem, index) => normalizeExpenseQueryRiskItem(riskItem, index))
.filter(Boolean),
summary: reason || `${expenseTypeLabel}报销`,
dateDisplay: documentDate || occurredAt || '待补充日期'
}
}
export function resolveExpenseStatusGroup(status) {
const normalized = String(status || '').trim()
if (['draft', 'supplement', 'returned'].includes(normalized)) {
return { key: 'draft', label: normalized === 'draft' ? '草稿' : '待完善' }
}
if (['submitted', 'review'].includes(normalized)) {
return { key: 'in_progress', label: '审批中' }
}
if (normalized === 'pending_payment') {
return { key: 'pending_payment', label: '待付款' }
}
if (normalized === 'paid') {
return { key: 'completed', label: '已付款' }
}
if (['approved'].includes(normalized)) {
return { key: 'completed', label: '已完成' }
}
return { key: 'other', label: '其他状态' }
}
export function formatQueryRecordDate(value) {
const text = String(value || '').trim()
if (!text) return ''
return text.includes('T') ? text.split('T')[0] : text.slice(0, 10)
}
export function buildQueryRecordFromClaim(claim) {
if (!claim || typeof claim !== 'object') {
return null
}
const claimId = String(claim.id || claim.claim_id || '').trim()
if (!claimId) {
return null
}
const status = String(claim.status || '').trim()
const statusGroup = resolveExpenseStatusGroup(status)
return {
claim_id: claimId,
claim_no: String(claim.claim_no || claim.claimNo || '').trim() || '未编号',
employee_name: String(claim.employee_name || claim.employeeName || '').trim(),
expense_type: String(claim.expense_type || claim.expenseType || '').trim(),
expense_type_label: EXPENSE_TYPE_LABELS[String(claim.expense_type || claim.expenseType || '').trim()] || String(claim.expense_type || claim.expenseType || '报销').trim(),
amount: Number(claim.amount || 0),
status,
status_label: EXPENSE_STATUS_LABELS[status] || statusGroup.label,
status_group: statusGroup.key,
status_group_label: statusGroup.label,
approval_stage: String(claim.approval_stage || claim.approvalStage || '').trim(),
document_date: formatQueryRecordDate(claim.submitted_at || claim.submittedAt || claim.created_at || claim.createdAt || claim.occurred_at || claim.occurredAt),
occurred_at: formatQueryRecordDate(claim.occurred_at || claim.occurredAt),
reason: String(claim.reason || '').trim(),
location: String(claim.location || '').trim()
}
}
export function buildDraftAssociationQueryPayload(claims) {
const records = (Array.isArray(claims) ? claims : [])
.filter((claim) => ASSOCIATABLE_CLAIM_STATUSES.has(String(claim?.status || '').trim()))
.map(buildQueryRecordFromClaim)
.filter(Boolean)
const statusGroups = records.reduce((groups, record) => {
const key = String(record.status_group || 'other')
const existing = groups.get(key) || {
key,
label: String(record.status_group_label || '其他状态'),
count: 0
}
existing.count += 1
groups.set(key, existing)
return groups
}, new Map())
return normalizeExpenseQueryPayload({
result_type: 'expense_claim_list',
title: '选择关联草稿',
scope_label: '可关联草稿',
selection_mode: 'draft_association',
empty_text: '当前没有可关联的草稿单据。',
recent_window_applied: false,
record_count: records.length,
preview_count: records.length,
total_amount: records.reduce((sum, record) => sum + Number(record.amount || 0), 0),
status_groups: Array.from(statusGroups.values()),
records
})
}
export function normalizeExpenseQueryPayload(payload) {
if (!payload || typeof payload !== 'object') {
return null
}
const resultType = String(payload.result_type || '').trim()
if (resultType && resultType !== 'expense_claim_list') {
return null
}
const records = (Array.isArray(payload.records) ? payload.records : [])
.map(normalizeExpenseQueryRecord)
.filter(Boolean)
const statusGroups = (Array.isArray(payload.status_groups) ? payload.status_groups : [])
.map(normalizeExpenseQueryStatusGroup)
.filter(Boolean)
const rawRecordCount = Number(payload.record_count || 0)
const rawPreviewCount = Number(payload.preview_count || records.length)
const rawPreviewLimit = Number(payload.preview_limit || EXPENSE_QUERY_PAGE_SIZE)
const rawOlderRecordCount = Number(payload.older_record_count || 0)
const totalAmount = Number(payload.total_amount || 0)
const rawWindowDays = Number(payload.window_days || 0)
const windowStartDate = String(payload.window_start_date || '').trim()
const windowEndDate = String(payload.window_end_date || '').trim()
return {
resultType: 'expense_claim_list',
scopeLabel: String(payload.scope_label || '报销单').trim() || '报销单',
selectionMode: String(payload.selection_mode || payload.selectionMode || '').trim(),
selectionLocked: Boolean(payload.selection_locked || payload.selectionLocked),
selectedClaimId: String(payload.selected_claim_id || payload.selectedClaimId || '').trim(),
title: String(payload.title || '').trim(),
emptyText: String(payload.empty_text || payload.emptyText || '').trim(),
recentWindowApplied: Boolean(payload.recent_window_applied),
windowDays:
payload.window_days === null || payload.window_days === undefined || payload.window_days === ''
? null
: (Number.isFinite(rawWindowDays) ? Math.max(1, rawWindowDays) : null),
windowStartDate: windowStartDate || '',
windowEndDate: windowEndDate || '',
recordCount: Number.isFinite(rawRecordCount) ? Math.max(0, rawRecordCount) : 0,
previewCount: Number.isFinite(rawPreviewCount) ? Math.max(0, rawPreviewCount) : records.length,
previewLimit: Number.isFinite(rawPreviewLimit) ? Math.max(1, rawPreviewLimit) : EXPENSE_QUERY_PAGE_SIZE,
olderRecordCount: Number.isFinite(rawOlderRecordCount) ? Math.max(0, rawOlderRecordCount) : 0,
hasMoreInWindow: Boolean(payload.has_more_in_window || payload.has_more),
totalAmount: Number.isFinite(totalAmount) ? totalAmount : 0,
statusGroups,
records,
currentPage: 1
}
}
export function buildExpenseQueryWindowLabel(queryPayload) {
if (!queryPayload) {
return ''
}
if (queryPayload.selectionMode === 'draft_association') {
return '先选择要关联的草稿,确认后我再识别附件并归集到该单据。'
}
if (queryPayload.windowStartDate && queryPayload.windowEndDate) {
return `${queryPayload.windowStartDate}${queryPayload.windowEndDate}`
}
if (queryPayload.recentWindowApplied && queryPayload.windowDays) {
return `${queryPayload.windowDays} 日内`
}
return '当前条件下'
}
export function getExpenseQueryTotalPages(queryPayload) {
const recordCount = Array.isArray(queryPayload?.records) ? queryPayload.records.length : 0
return Math.max(1, Math.ceil(recordCount / EXPENSE_QUERY_PAGE_SIZE))
}
export function getExpenseQueryActivePage(queryPayload) {
const totalPages = getExpenseQueryTotalPages(queryPayload)
const rawPage = Number(queryPayload?.currentPage || 1)
if (!Number.isFinite(rawPage)) {
return 1
}
return Math.min(Math.max(1, Math.round(rawPage)), totalPages)
}
export function getExpenseQueryVisibleRecords(queryPayload) {
const records = Array.isArray(queryPayload?.records) ? queryPayload.records : []
const activePage = getExpenseQueryActivePage(queryPayload)
const start = (activePage - 1) * EXPENSE_QUERY_PAGE_SIZE
return records.slice(start, start + EXPENSE_QUERY_PAGE_SIZE)
}
export function buildExpenseQueryHint(queryPayload) {
if (!queryPayload) {
return ''
}
if (queryPayload.selectionMode === 'draft_association') {
if (queryPayload.selectionLocked && queryPayload.selectedClaimId) {
return '已选择关联草稿,附件将按该单据继续识别和归集。'
}
return '如果这些都不是本次要关联的单据,可以补充单号或先到单据中心新建草稿。'
}
const parts = []
const previewLimit = Math.max(1, Number(queryPayload.previewLimit || EXPENSE_QUERY_PAGE_SIZE))
const totalCount = Math.max(0, Number(queryPayload.recordCount || 0))
if (totalCount > previewLimit) {
parts.push(`我只会筛选出最近的 ${previewLimit} 条记录;如果想查询全部的单据,请点击 [**这里**](${EXPENSE_CENTER_HREF}) 跳转到单据中心查看。`)
} else if (totalCount > 0) {
parts.push(`当前已展示本次筛选命中的全部记录;如果想进入单据中心继续筛选,请点击 [**这里**](${EXPENSE_CENTER_HREF})。`)
}
return parts.join('。')
}