function parseNumber(value) { const nextValue = Number(value) return Number.isFinite(nextValue) ? nextValue : 0 } function normalizeText(value) { return String(value ?? '').trim() } function toDate(value) { if (!value) { return null } const nextDate = new Date(value) return Number.isNaN(nextDate.getTime()) ? null : nextDate } function isCurrentMonth(dateValue) { const date = toDate(dateValue) if (!date) { return false } const now = new Date() return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() } function resolveClaimDate(request) { return request?.submittedAt || request?.createdAt || request?.occurredAt || '' } export function belongsToCurrentUser(request, currentUser) { const person = String(request?.person || request?.employeeName || '').trim() if (!person) { return false } const names = [ String(currentUser?.name || '').trim(), String(currentUser?.username || '').trim() ].filter(Boolean) return names.some((name) => name === person) } export function hasHighRiskFlag(request) { const riskFlags = Array.isArray(request?.riskFlags) ? request.riskFlags : [] if (riskFlags.some((item) => String(item?.severity || '').trim().toLowerCase() === 'high')) { return true } const summary = String(request?.riskSummary || '').trim() return summary.includes('高') } function formatCurrency(value) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(value) ? 0 : 2 }).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)) : [] const monthlyClaims = ownedRequests.filter((item) => isCurrentMonth(resolveClaimDate(item))) const monthlyCount = monthlyClaims.length const monthlyAmount = monthlyClaims.reduce((sum, item) => sum + parseNumber(item.amount), 0) const totalCount = ownedRequests.length const totalAmount = ownedRequests.reduce((sum, item) => sum + parseNumber(item.amount), 0) const inReviewCount = ownedRequests.filter((item) => item.approvalKey === 'in_progress').length const pendingPaymentCount = ownedRequests.filter((item) => { const status = String(item.status || item.approvalStatus || '').trim() return status.includes('待付款') || status.includes('待支付') }).length 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, monthlyAmount, monthlyAmountLabel: formatCurrency(monthlyAmount), totalCount, totalAmount, totalAmountLabel: formatCurrency(totalAmount), inReviewCount, pendingPaymentCount, completedCount, returnCount, highRiskCount, todoItems, progressItems, notifications, unreadNotificationCount: notifications.filter((item) => item.unread).length } }