feat: 数字员工财务报告体系与定时提醒及看板快照调度

- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 09:25:23 +08:00
parent 0c74b4ab4a
commit 15006a05a7
114 changed files with 7356 additions and 650 deletions

View File

@@ -3,6 +3,10 @@ function parseNumber(value) {
return Number.isFinite(nextValue) ? nextValue : 0
}
function normalizeText(value) {
return String(value ?? '').trim()
}
function toDate(value) {
if (!value) {
return null
@@ -60,6 +64,174 @@ function formatCurrency(value) {
}).format(parseNumber(value))
}
function resolveRequestIdentity(request) {
return normalizeText(request?.claimNo || request?.claim_no || request?.id || request?.claimId)
}
function resolveRequestTarget(request) {
return {
type: 'document',
id: normalizeText(request?.claimId || request?.id),
claimNo: resolveRequestIdentity(request)
}
}
function resolveStatusTone(approvalKey) {
if (approvalKey === 'supplement' || approvalKey === 'rejected') return 'danger'
if (approvalKey === 'draft') return 'success'
if (approvalKey === 'pending_payment') return 'warning'
if (approvalKey === 'in_progress') return 'info'
return 'muted'
}
function resolveTodoAction(request) {
const approvalKey = normalizeText(request?.approvalKey)
const status = normalizeText(request?.status || request?.approvalStatus)
if (approvalKey === 'supplement' || approvalKey === 'rejected') {
return {
title: '补充或修改单据',
status: approvalKey === 'rejected' ? '退回修改' : '待补充',
statusTone: 'danger',
iconKey: 'receipts',
color: 'var(--danger)',
accent: 'var(--danger-soft)'
}
}
if (approvalKey === 'draft' || /draft|草稿|待提交/i.test(status)) {
return {
title: '提交草稿单据',
status: '待提交',
statusTone: 'success',
iconKey: 'travelDraft',
color: 'var(--theme-primary)',
accent: 'var(--theme-primary-soft)'
}
}
return null
}
function buildTodoItems(ownedRequests) {
return ownedRequests
.map((request) => {
const action = resolveTodoAction(request)
if (!action) {
return null
}
const requestId = resolveRequestIdentity(request)
const title = normalizeText(request?.title || request?.note || request?.sceneLabel) || requestId
return {
...action,
id: requestId,
requestId,
title: action.title,
description: `${requestId || '单据'} · ${title || '费用单据'}`,
due: normalizeText(request?.updatedAt || request?.applyTime || request?.submittedAt) || '待处理',
target: resolveRequestTarget(request),
prompt: `帮我处理 ${requestId || title}${action.status}`
}
})
.filter(Boolean)
.sort((left, right) => normalizeText(right.due).localeCompare(normalizeText(left.due)))
}
function resolveProgressStatusTone(approvalKey) {
if (approvalKey === 'completed') return 'muted'
if (approvalKey === 'pending_payment') return 'warning'
if (approvalKey === 'supplement' || approvalKey === 'rejected') return 'danger'
return 'success'
}
function resolveCurrentProgressIndex(steps) {
const currentIndex = steps.findIndex((step) => step?.current)
if (currentIndex >= 0) {
return currentIndex
}
const activeIndex = steps.findLastIndex((step) => step?.active || step?.done)
return Math.max(0, activeIndex)
}
export function buildAdjacentProgressSteps(steps = [], windowSize = 4) {
const rows = Array.isArray(steps) ? steps : []
if (!rows.length) {
return []
}
const currentIndex = resolveCurrentProgressIndex(rows)
const safeWindowSize = Math.max(1, Number(windowSize) || 4)
let start = Math.max(0, currentIndex - 1)
let end = Math.min(rows.length, start + safeWindowSize)
if (end - start < safeWindowSize) {
start = Math.max(0, end - safeWindowSize)
}
return rows.slice(start, end).map((step) => ({
label: normalizeText(step.label || step.rawLabel),
done: Boolean(step.done),
current: Boolean(step.current),
title: normalizeText(step.title || step.time || step.detail)
}))
}
function buildProgressItems(ownedRequests) {
return ownedRequests
.filter((request) => Array.isArray(request?.progressSteps) && request.progressSteps.length)
.map((request) => {
const requestId = resolveRequestIdentity(request)
const steps = buildAdjacentProgressSteps(request.progressSteps, 4)
const currentStep = steps.find((step) => step.current)
const title = normalizeText(request?.title || request?.note || request?.sceneLabel) || '费用单据'
return {
id: requestId,
requestId,
title,
amount: formatCurrency(request?.amount),
status: normalizeText(request?.approvalStatus || currentStep?.label) || '处理中',
statusTone: resolveProgressStatusTone(normalizeText(request?.approvalKey)),
updatedAt: normalizeText(request?.updatedAt || request?.submittedAt || request?.createdAt),
steps,
target: resolveRequestTarget(request),
prompt: `查询 ${requestId || title} 的费用进度`
}
})
.sort((left, right) => normalizeText(right.updatedAt).localeCompare(normalizeText(left.updatedAt)))
}
function buildNotifications(todoItems, progressItems) {
const todoNotifications = todoItems.map((item) => ({
id: `todo:${item.requestId || item.description}`,
title: item.status,
description: item.description,
time: item.due,
unread: true,
tone: item.statusTone,
target: item.target,
prompt: item.prompt
}))
const progressNotifications = progressItems
.filter((item) => ['danger', 'warning'].includes(item.statusTone))
.map((item) => ({
id: `progress:${item.requestId || item.title}`,
title: item.status,
description: `${item.requestId || '单据'} · ${item.title}`,
time: item.updatedAt || '最近更新',
unread: false,
tone: item.statusTone,
target: item.target,
prompt: item.prompt
}))
return [...todoNotifications, ...progressNotifications]
}
export function buildWorkbenchSummary(requests, currentUser) {
const ownedRequests = Array.isArray(requests)
? requests.filter((item) => belongsToCurrentUser(item, currentUser))
@@ -79,6 +251,9 @@ export function buildWorkbenchSummary(requests, currentUser) {
const completedCount = ownedRequests.filter((item) => item.approvalKey === 'completed').length
const returnCount = ownedRequests.filter((item) => item.approvalKey === 'rejected').length
const highRiskCount = monthlyClaims.filter((item) => hasHighRiskFlag(item)).length
const todoItems = buildTodoItems(ownedRequests)
const progressItems = buildProgressItems(ownedRequests)
const notifications = buildNotifications(todoItems, progressItems)
return {
monthlyCount,
@@ -91,6 +266,10 @@ export function buildWorkbenchSummary(requests, currentUser) {
pendingPaymentCount,
completedCount,
returnCount,
highRiskCount
highRiskCount,
todoItems,
progressItems,
notifications,
unreadNotificationCount: notifications.filter((item) => item.unread).length
}
}