import { computed, reactive, ref } from 'vue' import { fetchExpenseClaims } from '../services/reimbursements.js' const EXPENSE_TYPE_LABELS = { travel: '差旅费', entertainment: '业务招待费', office: '办公费', meeting: '会务费', training: '培训费', hotel: '住宿费', transport: '交通费', meal: '餐费', other: '其他费用' } const REIMBURSEMENT_PROGRESS_LABELS = [ '保存草稿', '待提交', 'AI验审', '直属领导审批', '财务审批', '归档入账' ] function parseNumber(value) { const nextValue = Number(value) return Number.isFinite(nextValue) ? nextValue : 0 } function toDate(value) { if (!value) { return null } const nextDate = new Date(value) return Number.isNaN(nextDate.getTime()) ? null : nextDate } function formatDate(value) { const nextDate = toDate(value) if (!nextDate) { return '' } const year = nextDate.getFullYear() const month = String(nextDate.getMonth() + 1).padStart(2, '0') const day = String(nextDate.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function formatDateTime(value) { const nextDate = toDate(value) if (!nextDate) { return '' } const hours = String(nextDate.getHours()).padStart(2, '0') const minutes = String(nextDate.getMinutes()).padStart(2, '0') return `${formatDate(nextDate)} ${hours}:${minutes}` } function formatAmount(value) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(value) ? 0 : 2 }).format(parseNumber(value)) } function resolveTypeLabel(typeCode) { return EXPENSE_TYPE_LABELS[String(typeCode || '').trim()] || EXPENSE_TYPE_LABELS.other } function resolveApprovalMeta(status) { const normalized = String(status || '').trim().toLowerCase() if (normalized === 'draft') { return { key: 'draft', label: '草稿', tone: 'draft' } } if (['supplement', 'returned'].includes(normalized)) { return { key: 'supplement', label: '待补充', tone: 'warning' } } if (['approved', 'completed', 'paid'].includes(normalized)) { return { key: 'completed', label: '已完成', tone: 'success' } } if (['rejected', 'cancelled'].includes(normalized)) { return { key: 'rejected', label: '已退回', tone: 'danger' } } return { key: 'in_progress', label: '审批中', tone: 'info' } } function resolveWorkflowNode(claim, approvalMeta) { const rawNode = String(claim?.approval_stage || '').trim() if (rawNode) { if (rawNode === '审批流转') { return 'AI验审' } if (rawNode === '待补充') { return approvalMeta.key === 'draft' ? '待提交' : 'AI验审' } return rawNode } if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') { return '待提交' } if (approvalMeta.key === 'completed') { return '归档入账' } return 'AI验审' } function stringifyRiskFlag(value) { if (typeof value === 'string') { return value.trim() } if (!value || typeof value !== 'object') { return '' } for (const key of ['message', 'label', 'reason', 'name']) { const nextValue = String(value[key] || '').trim() if (nextValue) { return nextValue } } return '' } function buildRiskSummary(riskFlags) { if (!Array.isArray(riskFlags) || !riskFlags.length) { return '无' } const items = riskFlags.map((item) => stringifyRiskFlag(item)).filter(Boolean) return items.length ? items.join(';') : '无' } function buildOccurredDisplay(claim) { const itemDates = Array.isArray(claim?.items) ? claim.items.map((item) => formatDate(item?.item_date)).filter(Boolean) : [] if (!itemDates.length) { return formatDate(claim?.occurred_at) || '待补充' } const sortedDates = [...new Set(itemDates)].sort() if (sortedDates.length === 1) { return sortedDates[0] } return `${sortedDates[0]} ~ ${sortedDates[sortedDates.length - 1]}` } function resolveProgressCurrentIndex(approvalMeta, workflowNode) { const normalizedNode = String(workflowNode || '').trim() if (approvalMeta.key === 'completed') { return 5 } if (normalizedNode.includes('归档') || normalizedNode.includes('入账')) { return 5 } if (normalizedNode.includes('财务')) { return 4 } if ( normalizedNode.includes('直属领导') || normalizedNode.includes('领导审批') || normalizedNode.includes('部门负责人') || normalizedNode.includes('负责人审批') ) { return 3 } if (normalizedNode.includes('AI验审') || normalizedNode.includes('审批流转')) { return 2 } if (normalizedNode.includes('待提交')) { return 1 } if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') { return 1 } return 2 } function buildProgressSteps(approvalMeta, workflowNode) { const currentIndex = resolveProgressCurrentIndex(approvalMeta, workflowNode) const currentTime = approvalMeta.key === 'completed' ? '已完成' : approvalMeta.key === 'supplement' ? '待补充' : approvalMeta.key === 'rejected' ? '已退回' : '进行中' return REIMBURSEMENT_PROGRESS_LABELS.map((label, index) => { if (approvalMeta.key === 'completed') { return { index: index + 1, label, time: '已完成', done: true, active: true, current: false } } if (index < currentIndex) { return { index: index + 1, label, time: '已完成', done: true, active: true, current: false } } if (index === currentIndex) { return { index: index + 1, label, time: currentTime, done: false, active: true, current: true } } return { index: index + 1, label, time: '待处理', done: false, active: false, current: false } }) } function buildExpenseItems(claim, riskSummary) { if (!Array.isArray(claim?.items)) { return [] } return claim.items.map((item, index) => { const attachments = String(item?.invoice_id || '').trim() ? [String(item.invoice_id).trim()] : [] const itemTypeLabel = resolveTypeLabel(item?.item_type || claim?.expense_type) return { id: String(item?.id || `${claim?.id || 'claim'}-item-${index}`), time: formatDate(item?.item_date) || '待补充', itemDate: formatDate(item?.item_date) || '', itemType: String(item?.item_type || claim?.expense_type || '').trim() || 'other', itemReason: String(item?.item_reason || claim?.reason || '').trim(), itemLocation: String(item?.item_location || claim?.location || '').trim(), itemAmount: parseNumber(item?.item_amount), invoiceId: String(item?.invoice_id || '').trim(), dayLabel: claim?.expense_type === 'travel' ? `第 ${index + 1} 项` : '业务发生项', name: itemTypeLabel, category: itemTypeLabel, desc: String(item?.item_reason || claim?.reason || '').trim() || '待补充', detail: String(item?.item_location || claim?.location || '').trim() || '待补充', amount: formatAmount(item?.item_amount), status: attachments.length ? '已识别' : '待补充', tone: attachments.length ? 'ok' : 'bad', attachmentStatus: attachments.length ? `${attachments.length} 份附件` : '待上传', attachmentHint: attachments.length ? attachments[0] : '暂无关联票据', attachmentTone: attachments.length ? 'ok' : 'missing', attachments, riskLabel: riskSummary === '无' ? '无' : '待关注', riskText: riskSummary === '无' ? '' : riskSummary, riskTone: riskSummary === '无' ? 'low' : 'medium' } }) } function mapExpenseClaimToRequest(claim) { const typeCode = String(claim?.expense_type || '').trim() || 'other' const typeLabel = resolveTypeLabel(typeCode) const approvalMeta = resolveApprovalMeta(claim?.status) const workflowNode = resolveWorkflowNode(claim, approvalMeta) const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count)) const riskSummary = buildRiskSummary(claim?.risk_flags_json) const expenseItems = buildExpenseItems(claim, riskSummary) const applyDateTime = claim?.submitted_at || claim?.created_at return { id: String(claim?.claim_no || claim?.id || '').trim(), claimNo: String(claim?.claim_no || claim?.id || '').trim(), claimId: String(claim?.id || '').trim(), status: String(claim?.status || '').trim(), person: String(claim?.employee_name || '').trim() || '待补充', dept: String(claim?.department_name || '').trim() || '待补充', departmentName: String(claim?.department_name || '').trim() || '待补充', employeeName: String(claim?.employee_name || '').trim() || '待补充', entity: '', typeCode, typeLabel, detailVariant: typeCode === 'travel' ? 'travel' : 'general', title: String(claim?.reason || '').trim() || `${typeLabel}报销`, sceneLabel: typeLabel, sceneTarget: String(claim?.location || '').trim() || '待补充', location: String(claim?.location || '').trim() || '待补充', relatedCustomer: '', occurredDisplay: buildOccurredDisplay(claim), occurredAt: claim?.occurred_at || '', applyTime: formatDateTime(applyDateTime) || '待补充', submittedAt: applyDateTime || '', createdAt: claim?.created_at || '', amount: parseNumber(claim?.amount), workflowNode, approvalKey: approvalMeta.key, approvalStatus: approvalMeta.label, approvalTone: approvalMeta.tone, secondaryStatusLabel: typeCode === 'travel' ? '行程状态' : '票据状态', secondaryStatusValue: invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据', secondaryStatusTone: invoiceCount > 0 ? 'success' : 'warning', riskSummary, attachmentSummary: invoiceCount > 0 ? `${invoiceCount} 张票据` : '无', expenseTableSummary: expenseItems.length ? (invoiceCount > 0 ? `共 ${expenseItems.length} 条费用明细,已关联 ${invoiceCount} 张票据` : `共 ${expenseItems.length} 条费用明细,待补充票据`) : '暂无费用明细', note: String(claim?.reason || '').trim(), progressSteps: buildProgressSteps(approvalMeta, workflowNode), expenseItems } } function getWeekStart(date) { const nextDate = new Date(date) const day = nextDate.getDay() || 7 nextDate.setHours(0, 0, 0, 0) nextDate.setDate(nextDate.getDate() - day + 1) return nextDate } function resolveRangeMatch(activeRange, item) { if (activeRange === 'custom' || activeRange === '本月') { if (activeRange !== '本月') { return true } } const targetDate = toDate(item?.submittedAt || item?.createdAt || item?.occurredAt) if (!targetDate) { return true } const now = new Date() const targetDay = formatDate(targetDate) if (activeRange === '今日') { return targetDay === formatDate(now) } if (activeRange === '本周') { const weekStart = getWeekStart(now) const nextWeekStart = new Date(weekStart) nextWeekStart.setDate(nextWeekStart.getDate() + 7) return targetDate >= weekStart && targetDate < nextWeekStart } if (activeRange === '本月') { return ( targetDate.getFullYear() === now.getFullYear() && targetDate.getMonth() === now.getMonth() ) } return true } export function useRequests() { const requests = ref([]) const loading = ref(false) const error = ref('') const search = ref('') const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' }) const ranges = ['今日', '本周', '本月'] const activeRange = ref('本周') const filteredRequests = computed(() => { const key = search.value.trim().toLowerCase() return requests.value.filter((item) => { const searchText = [ item.id, item.person, item.typeLabel, item.title, item.sceneTarget, item.riskSummary ] .filter(Boolean) .join('') .toLowerCase() const matchesSearch = !key || searchText.includes(key) const matchesRange = resolveRangeMatch(activeRange.value, item) return matchesSearch && matchesRange }) }) async function reload() { loading.value = true error.value = '' try { const payload = await fetchExpenseClaims() requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : [] } catch (nextError) { requests.value = [] error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。' } finally { loading.value = false } } function approveRequest(request) { return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。` } function rejectRequest(request) { return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。` } void reload() return { requests, loading, error, search, filters, ranges, activeRange, filteredRequests, approveRequest, rejectRequest, reload } }