diff --git a/web/src/data/requests.js b/web/src/data/requests.js index 16ab128..0e9a5d7 100644 --- a/web/src/data/requests.js +++ b/web/src/data/requests.js @@ -1,37 +1,72 @@ -export const initialRequests = [ - { id: 'REQ-2026-0418', person: '刘倩', dept: '销售 · 华东区域', entity: 'Northstar China Ltd.', range: '今日', category: '机票', amount: '¥8,460', verdict: '可通过但需备注', status: 'warning', sla: '19h', slaStatus: 'warning', risk: '改签说明缺失,公务舱价格高于差标 12%' }, - { id: 'REQ-2026-0422', person: '韩阳', dept: '解决方案 · 北区', entity: 'Northstar China Ltd.', range: '本周', category: '酒店', amount: '¥3,280', verdict: '等待补件', status: 'warning', sla: '27h', slaStatus: 'warning', risk: '缺少出差行程单,酒店单晚超标 8%' }, - { id: 'REQ-2026-0431', person: '王鑫', dept: '运营管理 · 总部', entity: 'Northstar Singapore Pte.', range: '本周', category: '火车/用车', amount: '¥1,224', verdict: '规则全通过', status: 'success', sla: '4h', slaStatus: 'success', risk: '无明显风险' }, - { id: 'REQ-2026-0436', person: '陈嘉', dept: '市场 · 品牌活动', entity: 'Northstar US Inc.', range: '本月', category: '餐补及杂费', amount: '¥2,680', verdict: '建议人工复核', status: 'danger', sla: '51h', slaStatus: 'danger', risk: '发票号码重复,疑似重复报销' }, - { id: 'REQ-2026-0441', person: '赵敏', dept: '研发 · 后端组', entity: 'Northstar China Ltd.', range: '今日', category: '酒店', amount: '¥2,940', verdict: '规则全通过', status: 'success', sla: '6h', slaStatus: 'success', risk: '无明显风险' }, - { id: 'REQ-2026-0443', person: '周晨', dept: '销售 · 华南区域', entity: 'Northstar China Ltd.', range: '本周', category: '机票', amount: '¥6,520', verdict: '建议人工复核', status: 'danger', sla: '33h', slaStatus: 'danger', risk: '航班时间与出差申请不一致' }, - { id: 'REQ-2026-0448', person: '李娜', dept: '客户成功 · 华北', entity: 'Northstar Singapore Pte.', range: '本周', category: '火车/用车', amount: '¥1,860', verdict: '规则全通过', status: 'success', sla: '5h', slaStatus: 'success', risk: '无明显风险' }, - { id: 'REQ-2026-0452', person: '孙博', dept: '采购中心', entity: 'Northstar US Inc.', range: '本月', category: '酒店', amount: '¥4,780', verdict: '等待补件', status: 'warning', sla: '29h', slaStatus: 'warning', risk: '缺少住宿发票原件' }, - { id: 'REQ-2026-0455', person: '马骁', dept: '市场 · 品牌活动', entity: 'Northstar US Inc.', range: '本月', category: '机票', amount: '¥7,340', verdict: '规则全通过', status: 'success', sla: '8h', slaStatus: 'success', risk: '无明显风险' }, - { id: 'REQ-2026-0458', person: '高宁', dept: '运营管理 · 总部', entity: 'Northstar Singapore Pte.', range: '今日', category: '餐补及杂费', amount: '¥980', verdict: '可通过但需备注', status: 'warning', sla: '11h', slaStatus: 'warning', risk: '餐补天数与行程存在 1 天偏差' }, - { id: 'REQ-2026-0462', person: '何川', dept: '解决方案 · 北区', entity: 'Northstar China Ltd.', range: '本月', category: '机票', amount: '¥5,460', verdict: '规则全通过', status: 'success', sla: '7h', slaStatus: 'success', risk: '无明显风险' }, - { id: 'REQ-2026-0466', person: '宋雨', dept: '研发 · 后端组', entity: 'Northstar China Ltd.', range: '本周', category: '酒店', amount: '¥3,660', verdict: '已退回补件', status: 'danger', sla: '41h', slaStatus: 'danger', risk: '入住城市与项目地点不一致' } -] +export const initialRequests = [] export const documents = [ - { id: 'DOC-2026-0401', type: '出差申请', typeTag: 'travel', applicant: '刘倩', dept: '销售 · 华东区域', date: '2026-04-18', amount: '¥8,460', status: '审批中', statusClass: 'warning', conclusion: '待审核', destination: '上海→杭州', days: 3 }, - { id: 'DOC-2026-0402', type: '酒店预订', typeTag: 'hotel', applicant: '韩阳', dept: '解决方案 · 北区', date: '2026-04-22', amount: '¥1,280', status: '已通过', statusClass: 'success', conclusion: '规则全通过', destination: '北京·望京凯悦', days: 2 }, - { id: 'DOC-2026-0403', type: '机票预订', typeTag: 'flight', applicant: '王鑫', dept: '运营管理 · 总部', date: '2026-04-25', amount: '¥2,340', status: '已完成', statusClass: 'success', conclusion: '合规无风险', destination: '北京→深圳', days: 1 }, - { id: 'DOC-2026-0404', type: '出差申请', typeTag: 'travel', applicant: '陈嘉', dept: '市场 · 品牌活动', date: '2026-04-26', amount: '¥12,680', status: '待补件', statusClass: 'danger', conclusion: '需补充行程说明', destination: '上海→成都', days: 4 }, - { id: 'DOC-2026-0405', type: '火车票预订', typeTag: 'train', applicant: '赵敏', dept: '研发 · 后端组', date: '2026-04-27', amount: '¥553', status: '审批中', statusClass: 'warning', conclusion: '待审核', destination: '杭州→南京', days: 1 }, - { id: 'DOC-2026-0406', type: '酒店预订', typeTag: 'hotel', applicant: '刘倩', dept: '销售 · 华东区域', date: '2026-04-28', amount: '¥2,100', status: '已退回', statusClass: 'danger', conclusion: '住宿超标 23%', destination: '杭州·西湖国宾馆', days: 2 }, - { id: 'DOC-2026-0407', type: '机票预订', typeTag: 'flight', applicant: '韩阳', dept: '解决方案 · 北区', date: '2026-04-28', amount: '¥3,800', status: '已通过', statusClass: 'success', conclusion: '规则全通过', destination: '北京→广州', days: 1 }, - { id: 'DOC-2026-0408', type: '出差申请', typeTag: 'travel', applicant: '王鑫', dept: '运营管理 · 总部', date: '2026-04-29', amount: '¥4,200', status: '审批中', statusClass: 'warning', conclusion: '预算校验中', destination: '深圳→厦门', days: 2 } + { + id: 'DOC-2026-0501', + type: '个人报销单', + typeTag: 'expense', + applicant: '刘倩', + dept: '销售 · 华东区域', + date: '2026-05-12', + amount: '¥8,460', + status: '审批中', + statusClass: 'warning', + conclusion: '待审核', + destination: '上海 → 杭州', + days: 3 + }, + { + id: 'DOC-2026-0502', + type: '业务招待费', + typeTag: 'entertainment', + applicant: '韩阳', + dept: '解决方案 · 华南区', + date: '2026-05-09', + amount: '¥2,860', + status: '待补件', + statusClass: 'danger', + conclusion: '缺参与人员名单', + destination: '深圳福田', + days: 1 + }, + { + id: 'DOC-2026-0503', + type: '办公费报销', + typeTag: 'office', + applicant: '赵敏', + dept: '研发 · 平台组', + date: '2026-05-10', + amount: '¥1,280', + status: '草稿', + statusClass: 'warning', + conclusion: '待提交', + destination: '上海张江园区', + days: 1 + }, + { + id: 'DOC-2026-0504', + type: '会务费报销', + typeTag: 'meeting', + applicant: '陈嘉', + dept: '市场 · 品牌活动', + date: '2026-05-06', + amount: '¥6,520', + status: '已完成', + statusClass: 'success', + conclusion: '规则全通过', + destination: '苏州国际会议中心', + days: 2 + } ] -export const docTypes = ['全部类型', '出差申请', '机票预订', '酒店预订', '火车票预订'] -export const docStatuses = ['全部状态', '审批中', '已通过', '已完成', '待补件', '已退回'] -export const docMonths = ['2026-04', '2026-03', '2026-02', '2026-01'] +export const docTypes = ['全部类型', '个人报销单', '业务招待费', '办公费报销', '会务费报销'] +export const docStatuses = ['全部状态', '草稿', '审批中', '已完成', '待补件'] +export const docMonths = ['2026-05', '2026-04', '2026-03', '2026-02'] -export const prompts = ['生成审批意见', '列出补件清单', '解释为什么拦截', '生成审计摘要'] +export const prompts = ['生成审批意见', '列出补件清单', '解释风险原因', '生成沟通摘要'] export const initialMessages = [ - { id: 1, role: 'agent', text: '我已读取单据、发票、行程和公司差旅制度。当前建议:可通过,但需要补充改签说明。' }, + { id: 1, role: 'agent', text: '我已读取单据、发票和公司费用报销制度。当前建议:可以继续处理,但仍需补充部分说明。' }, { id: 2, role: 'user', text: '请列出这张单据的主要风险。' }, - { id: 3, role: 'agent', text: '主要风险:缺少改签说明,且舱位价格高于差旅标准 12%。' } + { id: 3, role: 'agent', text: '主要风险包括:字段缺失、票据不完整,或金额与制度标准存在偏差。' } ] diff --git a/web/src/utils/requestViewModel.js b/web/src/utils/requestViewModel.js index 470dd7c..7c93edb 100644 --- a/web/src/utils/requestViewModel.js +++ b/web/src/utils/requestViewModel.js @@ -1,5 +1,86 @@ +const REQUEST_TYPE_META = { + travel: { + label: '差旅费', + detailVariant: 'travel', + tone: 'travel', + secondaryStatusLabel: '行程状态' + }, + entertainment: { + label: '业务招待费', + detailVariant: 'general', + tone: 'entertainment', + secondaryStatusLabel: '票据状态' + }, + hotel: { + label: '住宿费', + detailVariant: 'general', + tone: 'travel', + secondaryStatusLabel: '票据状态' + }, + transport: { + label: '交通费', + detailVariant: 'general', + tone: 'travel', + secondaryStatusLabel: '票据状态' + }, + meal: { + label: '餐费', + detailVariant: 'general', + tone: 'meeting', + secondaryStatusLabel: '票据状态' + }, + office: { + label: '办公费', + detailVariant: 'general', + tone: 'office', + secondaryStatusLabel: '票据状态' + }, + meeting: { + label: '会务费', + detailVariant: 'general', + tone: 'meeting', + secondaryStatusLabel: '票据状态' + }, + training: { + label: '培训费', + detailVariant: 'general', + tone: 'training', + secondaryStatusLabel: '票据状态' + }, + other: { + label: '其他费用', + detailVariant: 'general', + tone: 'other', + secondaryStatusLabel: '票据状态' + } +} + +const APPROVAL_META = { + draft: { label: '草稿', tone: 'draft' }, + in_progress: { label: '审批中', tone: 'info' }, + supplement: { label: '待补充', tone: 'warning' }, + completed: { label: '已完成', tone: 'success' }, + rejected: { label: '已退回', tone: 'danger' } +} + +const BACKEND_STATUS_META = { + draft: { key: 'draft', label: '草稿', tone: 'draft' }, + submitted: { key: 'in_progress', label: '审批中', tone: 'info' }, + pending: { key: 'in_progress', label: '审批中', tone: 'info' }, + reviewing: { key: 'in_progress', label: '审批中', tone: 'info' }, + in_review: { key: 'in_progress', label: '审批中', tone: 'info' }, + in_progress: { key: 'in_progress', label: '审批中', tone: 'info' }, + approved: { key: 'completed', label: '已完成', tone: 'success' }, + paid: { key: 'completed', label: '已完成', tone: 'success' }, + completed: { key: 'completed', label: '已完成', tone: 'success' }, + supplement: { key: 'supplement', label: '待补充', tone: 'warning' }, + returned: { key: 'supplement', label: '待补充', tone: 'warning' }, + rejected: { key: 'rejected', label: '已退回', tone: 'danger' }, + cancelled: { key: 'rejected', label: '已退回', tone: 'danger' } +} + function parseRequestDateFromId(id) { - const match = String(id || '').match(/^REQ-(\d{4})-(\d{2})(\d{2})$/) + const match = String(id || '').match(/(\d{4})[-]?(\d{2})(\d{2})/) if (!match) { return '' @@ -9,55 +90,85 @@ function parseRequestDateFromId(id) { return `${year}-${month}-${day}` } -function formatTripWindow(range) { - const normalized = String(range || '') - - if (!normalized) { - return '待补充' +function parseAmount(value) { + if (typeof value === 'number' && Number.isFinite(value)) { + return value } - if (normalized.includes('本月')) { - return '本月申请' + const normalized = String(value || '') + .replace(/[,,\s]/g, '') + .replace(/[¥¥]/g, '') + .replace(/元/g, '') + .trim() + + if (!normalized || !/^-?\d+(?:\.\d+)?$/.test(normalized)) { + return null } - if (normalized.includes('本周')) { - return '本周申请' - } - - if (normalized.includes('今天')) { - return '今日申请' - } - - return normalized + const amount = Number(normalized) + return Number.isFinite(amount) ? amount : null } -function mapApproval(status) { - if (status === 'success') { +export function formatRequestCurrency(value) { + const amount = parseAmount(value) + if (amount === null) { + return String(value || '').trim() || '待补充' + } + + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + minimumFractionDigits: 0, + maximumFractionDigits: Number.isInteger(amount) ? 0 : 2 + }).format(amount) +} + +function resolveApprovalState(request) { + const normalizedStatus = String(request?.status || '').trim().toLowerCase() + + if (normalizedStatus && BACKEND_STATUS_META[normalizedStatus]) { + const meta = BACKEND_STATUS_META[normalizedStatus] return { - node: '已完成归档', - approval: '已完成', - approvalTone: 'success', - travel: '已完成行程', - travelTone: 'success' + key: meta.key, + label: request.approvalStatus || request.approval || meta.label, + tone: request.approvalTone || meta.tone } } - if (status === 'danger') { + if (request?.approvalKey && APPROVAL_META[request.approvalKey]) { return { - node: '异常待复核', - approval: '待处理', - approvalTone: 'danger', - travel: '存在异常', - travelTone: 'danger' + key: request.approvalKey, + label: request.approvalStatus || request.approval || APPROVAL_META[request.approvalKey].label, + tone: request.approvalTone || APPROVAL_META[request.approvalKey].tone } } + if (typeof request?.approval === 'string' && request.approval.trim()) { + return { + key: request.approvalKey || 'in_progress', + label: request.approval, + tone: request.approvalTone || 'info' + } + } + + if (typeof request?.status === 'string') { + if (request.status === 'success') return { key: 'completed', label: '已完成', tone: 'success' } + if (request.status === 'danger') return { key: 'supplement', label: '待补充', tone: 'warning' } + } + + return { key: 'in_progress', label: '审批中', tone: 'info' } +} + +function resolveTypeMeta(request) { + const typeCode = String(request?.typeCode || request?.expense_type || '').trim() || 'other' + const typeMeta = REQUEST_TYPE_META[typeCode] || REQUEST_TYPE_META.other + return { - node: '财务审核中', - approval: '审批中', - approvalTone: 'info', - travel: '待安排行程', - travelTone: 'warning' + typeCode, + typeLabel: String(request?.typeLabel || request?.category || '').trim() || typeMeta.label, + detailVariant: String(request?.detailVariant || '').trim() || typeMeta.detailVariant, + typeTone: typeMeta.tone, + secondaryStatusLabel: request?.secondaryStatusLabel || typeMeta.secondaryStatusLabel } } @@ -66,22 +177,60 @@ export function normalizeRequestForUi(request) { return null } - const applyTime = parseRequestDateFromId(request.id) || '2026-04-18' - const reason = `${request.category || '差旅'}申请` - const city = request.entity || '待补充' - const period = formatTripWindow(request.range) - const approvalState = mapApproval(request.status) + const { typeCode, typeLabel, detailVariant, typeTone, secondaryStatusLabel } = resolveTypeMeta(request) + const approvalState = resolveApprovalState(request) + const amountValue = parseAmount(request.amount) + const amountDisplay = formatRequestCurrency(amountValue ?? request.amount) + const title = String(request.title || request.reason || '').trim() || `${typeLabel}报销` + const sceneTarget = String(request.sceneTarget || request.location || request.city || request.entity || '').trim() || '待补充' + const occurredDisplay = String(request.occurredDisplay || request.period || request.occurredAt || '').trim() || '待补充' + const applyTime = String(request.applyTime || parseRequestDateFromId(request.id) || '').trim() || '待补充' + const workflowNode = String(request.workflowNode || request.node || '').trim() || '待提交' + const secondaryStatusValue = + String(request.secondaryStatusValue || request.travel || '').trim() + || (detailVariant === 'travel' ? '待安排行程' : '待补充票据') + const secondaryStatusTone = String(request.secondaryStatusTone || request.travelTone || '').trim() || 'neutral' return { ...request, - reason, - city, - period, + claimId: String(request.claimId || request.claim_id || '').trim(), + documentNo: String(request.documentNo || request.claimNo || request.claim_no || request.id || '').trim(), + typeCode, + typeLabel, + detailVariant, + typeTone, + title, + reason: title, + sceneLabel: String(request.sceneLabel || '').trim() || typeLabel, + sceneTarget, + city: sceneTarget, + location: String(request.location || '').trim() || sceneTarget, + relatedCustomer: String(request.relatedCustomer || '').trim() || '待补充', + occurredDisplay, + period: occurredDisplay, applyTime, - node: approvalState.node, - approval: approvalState.approval, - approvalTone: approvalState.approvalTone, - travel: approvalState.travel, - travelTone: approvalState.travelTone + amountValue, + amountDisplay, + amount: amountDisplay, + workflowNode, + node: workflowNode, + approvalKey: approvalState.key, + approvalStatus: approvalState.label, + approval: approvalState.label, + approvalTone: approvalState.tone, + secondaryStatusLabel, + secondaryStatusValue, + secondaryStatusTone, + travel: secondaryStatusValue, + travelTone: secondaryStatusTone, + riskSummary: String(request.riskSummary || request.risk || '').trim() || '暂无异常', + attachmentSummary: String(request.attachmentSummary || '').trim() || '待补充', + rangeBucket: String(request.rangeBucket || request.range || '').trim() || '本周', + detailTitle: String(request.detailTitle || '').trim() || title, + note: String(request.note || '').trim(), + profileName: String(request.person || request.applicant || request.employeeName || '').trim() || '当前申请人', + profileDepartment: String(request.dept || request.department || request.departmentName || '').trim() || '所属部门', + profileAvatar: + String(request.person || request.applicant || request.employeeName || '申').trim().slice(0, 1) || '申' } }