feat(workbench): add expense stats detail modal
This commit is contained in:
@@ -64,6 +64,61 @@ function formatCurrency(value) {
|
||||
}).format(parseNumber(value))
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
const percent = Number(value)
|
||||
if (!Number.isFinite(percent)) {
|
||||
return '0%'
|
||||
}
|
||||
|
||||
return `${percent.toFixed(percent >= 10 || percent === 0 ? 0 : 1)}%`
|
||||
}
|
||||
|
||||
function padDatePart(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatDateTimeLabel(value) {
|
||||
if (value instanceof Date) {
|
||||
return [
|
||||
value.getFullYear(),
|
||||
padDatePart(value.getMonth() + 1),
|
||||
padDatePart(value.getDate())
|
||||
].join('-') + ` ${padDatePart(value.getHours())}:${padDatePart(value.getMinutes())}`
|
||||
}
|
||||
|
||||
const text = normalizeText(value)
|
||||
if (!text) {
|
||||
return '暂无时间'
|
||||
}
|
||||
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2}))?/.exec(text)
|
||||
if (match) {
|
||||
return match[4] ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}:${match[5]}` : `${match[1]}-${match[2]}-${match[3]}`
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function formatDurationLabel(milliseconds) {
|
||||
const duration = Number(milliseconds)
|
||||
if (!Number.isFinite(duration) || duration <= 0) {
|
||||
return '暂无耗时'
|
||||
}
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
|
||||
if (duration < hour) {
|
||||
return `${Math.max(1, Math.round(duration / minute))}分钟`
|
||||
}
|
||||
if (duration < day) {
|
||||
return `${Math.max(1, Math.round(duration / hour))}小时`
|
||||
}
|
||||
|
||||
return `${Math.ceil(duration / day)}天`
|
||||
}
|
||||
|
||||
function resolveRequestIdentity(request) {
|
||||
return normalizeText(request?.claimNo || request?.claim_no || request?.id || request?.claimId)
|
||||
}
|
||||
@@ -232,6 +287,177 @@ function buildNotifications(todoItems, progressItems) {
|
||||
return [...todoNotifications, ...progressNotifications]
|
||||
}
|
||||
|
||||
function resolveExpenseCategory(request) {
|
||||
const explicitCategory = normalizeText(
|
||||
request?.expenseCategory
|
||||
|| request?.expense_category
|
||||
|| request?.category
|
||||
|| request?.expenseType
|
||||
|| request?.expense_type
|
||||
|| request?.type
|
||||
)
|
||||
if (explicitCategory) {
|
||||
return explicitCategory
|
||||
}
|
||||
|
||||
const text = normalizeText([
|
||||
request?.sceneLabel,
|
||||
request?.title,
|
||||
request?.note,
|
||||
request?.description
|
||||
].filter(Boolean).join(' '))
|
||||
|
||||
if (/差旅|出差|交通|机票|火车|高铁|出租|网约车|住宿|酒店/i.test(text)) {
|
||||
return '差旅交通'
|
||||
}
|
||||
if (/招待|客户|餐饮|宴请|接待/i.test(text)) {
|
||||
return '业务招待'
|
||||
}
|
||||
if (/办公|采购|用品|设备|耗材/i.test(text)) {
|
||||
return '办公采购'
|
||||
}
|
||||
if (/培训|学习|课程|会议/i.test(text)) {
|
||||
return '培训会议'
|
||||
}
|
||||
if (/市场|活动|推广|宣传/i.test(text)) {
|
||||
return '市场活动'
|
||||
}
|
||||
|
||||
return '其他费用'
|
||||
}
|
||||
|
||||
function buildExpenseDistributionRows(ownedRequests) {
|
||||
const groups = new Map()
|
||||
|
||||
for (const request of ownedRequests) {
|
||||
const category = resolveExpenseCategory(request)
|
||||
const amount = parseNumber(request?.amount)
|
||||
const group = groups.get(category) || {
|
||||
key: category,
|
||||
label: category,
|
||||
count: 0,
|
||||
amount: 0
|
||||
}
|
||||
|
||||
group.count += 1
|
||||
group.amount += amount
|
||||
groups.set(category, group)
|
||||
}
|
||||
|
||||
const totalAmount = Array.from(groups.values()).reduce((sum, item) => sum + item.amount, 0)
|
||||
const rows = Array.from(groups.values())
|
||||
.sort((left, right) => right.amount - left.amount || right.count - left.count)
|
||||
.slice(0, 6)
|
||||
|
||||
return rows.map((item) => {
|
||||
const percent = totalAmount > 0 ? (item.amount / totalAmount) * 100 : 0
|
||||
return {
|
||||
...item,
|
||||
amountLabel: formatCurrency(item.amount),
|
||||
percent,
|
||||
percentLabel: formatPercent(percent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function collectRequestDates(request) {
|
||||
const candidates = [
|
||||
request?.createdAt,
|
||||
request?.submittedAt,
|
||||
request?.updatedAt,
|
||||
request?.applyTime,
|
||||
request?.occurredAt
|
||||
]
|
||||
|
||||
const steps = Array.isArray(request?.progressSteps) ? request.progressSteps : []
|
||||
for (const step of steps) {
|
||||
candidates.push(step?.time, step?.createdAt, step?.updatedAt, step?.timestamp)
|
||||
}
|
||||
|
||||
return candidates
|
||||
.map((item) => toDate(item))
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.getTime() - right.getTime())
|
||||
}
|
||||
|
||||
function buildExpenseProcessingRows(ownedRequests) {
|
||||
return ownedRequests
|
||||
.map((request) => {
|
||||
const dates = collectRequestDates(request)
|
||||
const requestId = resolveRequestIdentity(request)
|
||||
const title = normalizeText(request?.title || request?.note || request?.sceneLabel) || '费用单据'
|
||||
const startedAt = dates[0] || toDate(request?.createdAt || request?.submittedAt)
|
||||
const latestAt = dates[dates.length - 1] || toDate(request?.updatedAt || request?.submittedAt || request?.createdAt)
|
||||
const stepCount = Array.isArray(request?.progressSteps) ? request.progressSteps.length : 0
|
||||
|
||||
return {
|
||||
id: requestId || title,
|
||||
requestId,
|
||||
title,
|
||||
status: normalizeText(request?.approvalStatus || request?.status) || '处理中',
|
||||
statusTone: resolveProgressStatusTone(normalizeText(request?.approvalKey)),
|
||||
startedAt: startedAt ? formatDateTimeLabel(startedAt) : '暂无开始时间',
|
||||
updatedAt: latestAt ? formatDateTimeLabel(latestAt) : '暂无更新时间',
|
||||
durationLabel: startedAt && latestAt ? formatDurationLabel(latestAt.getTime() - startedAt.getTime()) : '暂无耗时',
|
||||
stepCount,
|
||||
sortTime: latestAt ? latestAt.getTime() : 0
|
||||
}
|
||||
})
|
||||
.sort((left, right) => right.sortTime - left.sortTime)
|
||||
.slice(0, 6)
|
||||
.map(({ sortTime, ...item }) => item)
|
||||
}
|
||||
|
||||
function buildExpenseOperationRows(todoItems, notifications, progressItems) {
|
||||
const rows = []
|
||||
|
||||
for (const item of todoItems) {
|
||||
rows.push({
|
||||
id: `todo:${item.requestId || item.description}`,
|
||||
action: item.status || item.title,
|
||||
detail: item.description,
|
||||
time: item.due,
|
||||
tone: item.statusTone || 'info',
|
||||
source: '待办'
|
||||
})
|
||||
}
|
||||
|
||||
for (const item of notifications) {
|
||||
rows.push({
|
||||
id: `notice:${item.id}`,
|
||||
action: item.title,
|
||||
detail: item.description,
|
||||
time: item.time,
|
||||
tone: item.tone || 'info',
|
||||
source: item.unread ? '未读提醒' : '系统提醒'
|
||||
})
|
||||
}
|
||||
|
||||
for (const item of progressItems.slice(0, 6)) {
|
||||
rows.push({
|
||||
id: `progress:${item.requestId || item.id}`,
|
||||
action: item.status,
|
||||
detail: `${item.requestId || item.id || '单据'} · ${item.title}`,
|
||||
time: item.updatedAt || '最近更新',
|
||||
tone: item.statusTone || 'info',
|
||||
source: '进度'
|
||||
})
|
||||
}
|
||||
|
||||
const seen = new Set()
|
||||
return rows
|
||||
.filter((item) => {
|
||||
const key = [item.action, item.detail, item.time].join('|')
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.sort((left, right) => normalizeText(right.time).localeCompare(normalizeText(left.time)))
|
||||
.slice(0, 8)
|
||||
}
|
||||
|
||||
export function buildWorkbenchSummary(requests, currentUser) {
|
||||
const ownedRequests = Array.isArray(requests)
|
||||
? requests.filter((item) => belongsToCurrentUser(item, currentUser))
|
||||
@@ -254,6 +480,11 @@ export function buildWorkbenchSummary(requests, currentUser) {
|
||||
const todoItems = buildTodoItems(ownedRequests)
|
||||
const progressItems = buildProgressItems(ownedRequests)
|
||||
const notifications = buildNotifications(todoItems, progressItems)
|
||||
const expenseStatsDetail = {
|
||||
distributionRows: buildExpenseDistributionRows(ownedRequests),
|
||||
processingRows: buildExpenseProcessingRows(ownedRequests),
|
||||
operationRows: buildExpenseOperationRows(todoItems, notifications, progressItems)
|
||||
}
|
||||
|
||||
return {
|
||||
monthlyCount,
|
||||
@@ -270,6 +501,7 @@ export function buildWorkbenchSummary(requests, currentUser) {
|
||||
todoItems,
|
||||
progressItems,
|
||||
notifications,
|
||||
expenseStatsDetail,
|
||||
unreadNotificationCount: notifications.filter((item) => item.unread).length
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user