feat(web): 更新审批中心、审计、政策制度页面及对应的业务脚本,增强前端交互逻辑

This commit is contained in:
caoxiaozhu
2026-05-15 06:57:07 +00:00
parent 344ac126b3
commit 244b3a58f7
7 changed files with 1142 additions and 325 deletions

View File

@@ -1,59 +1,354 @@
import { computed, ref } from 'vue'
import { computed, ref } from 'vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import { mapExpenseClaimToRequest } from '../../composables/useRequests.js'
import { useSystemState } from '../../composables/useSystemState.js'
import { fetchExpenseClaims } from '../../services/reimbursements.js'
const DEFAULT_SLA_HOURS = 24
const tabs = ['全部待审', '高风险', '即将超时', '已处理']
const filters = ['法人主体', '费用类型', '风险等级', '金额区间', '所属部门']
const RISK_LABELS = {
low: '低风险',
medium: '中风险',
high: '高风险'
}
function toDate(value) {
if (!value) {
return null
}
const nextDate = new Date(value)
return Number.isNaN(nextDate.getTime()) ? null : nextDate
}
function formatCurrency(value) {
const amount = Number(value)
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 0,
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
}).format(Number.isFinite(amount) ? amount : 0)
}
function resolveRiskTone(riskFlags, riskSummary) {
if (Array.isArray(riskFlags)) {
const severities = riskFlags
.map((item) => String(item?.severity || '').trim().toLowerCase())
.filter(Boolean)
if (severities.includes('high')) {
return 'high'
}
if (severities.includes('medium')) {
return 'medium'
}
if (severities.includes('low')) {
return 'low'
}
}
if (String(riskSummary || '').trim() && String(riskSummary || '').trim() !== '无') {
return 'medium'
}
return 'low'
}
function resolveRiskItems(request) {
const riskFlags = Array.isArray(request?.riskFlags) ? request.riskFlags : []
const items = riskFlags
.map((item) => {
const tone = resolveRiskTone([item], '')
const text = String(item?.message || item?.label || item?.reason || '').trim()
if (!text) {
return null
}
return {
text,
level: tone === 'high' ? '高' : tone === 'medium' ? '中' : '低',
tone,
icon: tone === 'high' ? 'mdi mdi-alert-circle' : tone === 'medium' ? 'mdi mdi-alert' : 'mdi mdi-shield-check'
}
})
.filter(Boolean)
if (items.length) {
return items
}
const summary = String(request?.riskSummary || '').trim()
if (summary && summary !== '无') {
return summary.split('').filter(Boolean).map((text) => ({
text,
level: '中',
tone: 'medium',
icon: 'mdi mdi-alert'
}))
}
return [
{
text: 'AI验审已通过当前未发现额外风险。',
level: '低',
tone: 'low',
icon: 'mdi mdi-shield-check'
}
]
}
function resolveAttachmentMeta(name) {
const normalized = String(name || '').trim()
const lowerName = normalized.toLowerCase()
if (lowerName.endsWith('.pdf')) {
return { icon: 'mdi mdi-file-pdf-box', iconClass: 'pdf' }
}
if (/\.(png|jpg|jpeg|webp|bmp)$/i.test(lowerName)) {
return { icon: 'mdi mdi-image', iconClass: 'img' }
}
return { icon: 'mdi mdi-file-document-outline', iconClass: 'file' }
}
function buildAttachments(expenseItems) {
const seen = new Set()
const attachments = []
for (const item of Array.isArray(expenseItems) ? expenseItems : []) {
for (const fileName of Array.isArray(item?.attachments) ? item.attachments : []) {
const normalized = String(fileName || '').trim()
if (!normalized || seen.has(normalized)) {
continue
}
seen.add(normalized)
attachments.push({
name: normalized,
size: '已识别',
...resolveAttachmentMeta(normalized)
})
}
}
if (attachments.length) {
return attachments
}
return [
{
name: '当前无附件',
size: '待补充',
icon: 'mdi mdi-file-document-outline',
iconClass: 'miss',
missing: true
}
]
}
function resolveSlaMeta(submittedAt) {
const startAt = toDate(submittedAt)
if (!startAt) {
return { label: '待处理', tone: 'safe', urgent: false }
}
const deadline = new Date(startAt.getTime() + DEFAULT_SLA_HOURS * 60 * 60 * 1000)
const diffMs = deadline.getTime() - Date.now()
if (diffMs <= 0) {
return { label: '已超时', tone: 'danger', urgent: true }
}
const diffHours = diffMs / (60 * 60 * 1000)
const diffMinutes = Math.max(1, Math.ceil(diffMs / (60 * 1000)))
const label = diffHours >= 1 ? `${diffHours.toFixed(diffHours >= 10 ? 0 : 1)}h` : `${diffMinutes}m`
if (diffHours <= 2) {
return { label, tone: 'danger', urgent: true }
}
if (diffHours <= 8) {
return { label, tone: 'warning', urgent: false }
}
return { label, tone: 'safe', urgent: false }
}
function buildHeroSummaryItems(request) {
return [
{ label: '单号', value: request.id || '-', icon: 'mdi mdi-pound-box-outline' },
{ label: '报销类型', value: request.typeLabel || '-', icon: 'mdi mdi-briefcase-outline' },
{ label: '业务地点', value: request.sceneTarget || '待补充', icon: 'mdi mdi-map-marker-outline' },
{ label: '发生时间', value: request.occurredDisplay || '待补充', icon: 'mdi mdi-calendar-range' },
{ label: '票据关联', value: request.attachmentSummary || '无', icon: 'mdi mdi-paperclip' },
{ label: '事由', value: request.title || '待补充', icon: 'mdi mdi-text-box-outline' }
]
}
function buildFlowItems(request) {
return Array.isArray(request?.progressSteps)
? request.progressSteps.map((item) => ({
label: item.label,
desc: item.current ? '当前处理节点' : item.done ? '已完成' : '待处理',
time: item.time,
icon: item.current ? 'mdi mdi-circle-slice-8' : item.done ? 'mdi mdi-check' : 'mdi mdi-circle-outline',
current: item.current,
pending: !item.done && !item.current
}))
: []
}
function canCurrentUserProcessRequest(request, currentUser) {
const node = String(request?.workflowNode || '').trim()
const roleCodes = Array.isArray(currentUser?.roleCodes) ? currentUser.roleCodes.filter(Boolean) : []
const currentName = String(currentUser?.name || '').trim()
const applicantName = String(request?.person || request?.employeeName || '').trim()
if (currentName && applicantName && currentName === applicantName) {
return false
}
if (currentUser?.isAdmin || roleCodes.includes('finance')) {
return node.includes('财务')
}
return (
node.includes('直属领导')
|| node.includes('领导审批')
|| node.includes('部门负责人')
|| node.includes('负责人审批')
)
}
function buildApprovalRow(request) {
const riskTone = resolveRiskTone(request.riskFlags, request.riskSummary)
const riskItems = resolveRiskItems(request)
const expenseItems = Array.isArray(request.expenseItems) ? request.expenseItems : []
const slaMeta = resolveSlaMeta(request.submittedAt || request.createdAt)
const statusTone = slaMeta.urgent ? 'urgent' : 'pending'
return {
...request,
applicant: request.person,
avatar: String(request.person || '?').trim().slice(0, 1) || '?',
department: request.dept,
type: request.typeLabel,
amount: formatCurrency(request.amount),
time: request.applyTime,
risk: RISK_LABELS[riskTone] || RISK_LABELS.low,
riskTone,
sla: slaMeta.label,
slaTone: slaMeta.tone,
node: request.workflowNode || '审批中',
status: statusTone === 'urgent' ? '即将超时' : '待审批',
statusTone,
spotlight: riskTone === 'high' || statusTone === 'urgent',
heroSummaryItems: buildHeroSummaryItems(request),
summaryItems: buildHeroSummaryItems(request).slice(2),
progressSteps: Array.isArray(request.progressSteps) ? request.progressSteps : [],
expenseItems,
attachments: buildAttachments(expenseItems),
riskItems,
flowItems: buildFlowItems(request)
}
}
export default {
name: 'ApprovalCenterView' ,
setup(props, { emit }) {
name: 'ApprovalCenterView',
components: {
TableEmptyState
},
setup() {
const { currentUser } = useSystemState()
const activeTab = ref('全部待审')
const selectedRow = ref(null)
const selectedClaimId = ref('')
const expandedExpenseId = ref(null)
const tabs = ['全部待审', '高风险', '即将超时', '已处理']
const filters = ['法人主体', '费用类型', '风险等级', '金额区间', '所属部门']
const listKeyword = ref('')
const rows = ref([])
const loading = ref(false)
const error = ref('')
const rows = [
{ id: 'RE240712001', applicant: '李文静', avatar: '李', department: '市场部', type: '差旅报销', amount: '¥3,680', time: '07-12 09:20', risk: '中风险', riskTone: 'medium', sla: '4.2h', slaTone: 'safe', node: '财务审批', status: '待审批', statusTone: 'pending' },
{ id: 'RE240712002', applicant: '王志强', avatar: '王', department: '销售部', type: '招待费', amount: '¥1,280', time: '07-12 08:15', risk: '低风险', riskTone: 'low', sla: '8.5h', slaTone: 'safe', node: '部门负责人', status: '待审批', statusTone: 'pending' },
{ id: 'RE240711098', applicant: '刘思雨', avatar: '刘', department: '市场部', type: '差旅报销', amount: '¥6,920', time: '07-11 18:46', risk: '高风险', riskTone: 'high', sla: '0.8h', slaTone: 'danger', node: '财务审批', status: '即将超时', statusTone: 'urgent', spotlight: true },
{ id: 'RE240711087', applicant: '陈晓琳', avatar: '陈', department: '行政部', type: '办公采购', amount: '¥860', time: '07-11 17:32', risk: '低风险', riskTone: 'low', sla: '6.1h', slaTone: 'safe', node: '预算校验', status: '待审批', statusTone: 'pending' },
{ id: 'RE240711076', applicant: '赵明', avatar: '赵', department: '研发中心', type: '其他费用', amount: '¥4,250', time: '07-11 15:10', risk: '中风险', riskTone: 'medium', sla: '2.4h', slaTone: 'warning', node: '部门负责人', status: '待审批', statusTone: 'pending' },
{ id: 'RE240711065', applicant: '孙楠', avatar: '孙', department: '财务部', type: '招待费', amount: '¥560', time: '07-11 13:42', risk: '低风险', riskTone: 'low', sla: '5.7h', slaTone: 'safe', node: '财务审批', status: '待审批', statusTone: 'pending' },
{ id: 'RE240711054', applicant: '周晓彤', avatar: '周', department: '市场部', type: '办公采购', amount: '¥2,150', time: '07-11 11:28', risk: '中风险', riskTone: 'medium', sla: '1.9h', slaTone: 'warning', node: '预算校验', status: '即将超时', statusTone: 'urgent' },
{ id: 'RE240711043', applicant: '吴磊', avatar: '吴', department: '销售部', type: '其他费用', amount: '¥980', time: '07-11 09:05', risk: '低风险', riskTone: 'low', sla: '7.3h', slaTone: 'safe', node: '部门负责人', status: '待审批', statusTone: 'pending' }
]
const visibleRows = computed(() => {
if (activeTab.value === '全部待审') return rows
if (activeTab.value === '高风险') return rows.filter((row) => row.risk === '高风险')
if (activeTab.value === '即将超时') return rows.filter((row) => row.status === '即将超时')
return rows.slice(0, 3).map((row) => ({ ...row, status: '已处理', statusTone: 'done' }))
const selectedRow = computed({
get() {
return rows.value.find((row) => row.claimId === selectedClaimId.value) || null
},
set(value) {
selectedClaimId.value = value?.claimId || ''
expandedExpenseId.value = null
}
})
const approvalSteps = [
{ index: 1, label: '提交申请', time: '07-11 08:46', done: true, active: true },
{ index: 2, label: '票据识别', time: '07-11 08:48', done: true, active: true },
{ index: 3, label: '费用归类', time: '07-11 08:49', done: true, active: true },
{ index: 4, label: '部门负责人审批', time: '07-11 11:28', done: true, active: true },
{ index: 5, label: '财务审批', time: '进行中', active: true, current: true },
{ index: 6, label: '归档入账', time: '待处理' }
]
const visibleRows = computed(() => {
let filteredRows = rows.value
const summaryItems = [
{ label: '行程', value: '北京 → 上海', icon: 'mdi mdi-map-marker-path' },
{ label: '出差区间', value: '07-10 至 07-11', icon: 'mdi mdi-clock-outline' },
{ label: '票据关联', value: '8 条明细 / 7 份材料', icon: 'mdi mdi-file-document-multiple-outline' },
{ label: '成本归属', value: '市场部 · CC-MKT-01', icon: 'mdi mdi-account-group-outline' },
{ label: '支付方式', value: '企业垫付', icon: 'mdi mdi-credit-card-outline' }
]
// 根据标签筛选
if (activeTab.value === '高风险') {
filteredRows = filteredRows.filter((row) => row.riskTone === 'high')
} else if (activeTab.value === '即将超时') {
filteredRows = filteredRows.filter((row) => row.statusTone === 'urgent')
} else if (activeTab.value === '已处理') {
filteredRows = []
}
const heroSummaryItems = computed(() => [
{ label: '单号', value: selectedRow.value?.id ?? '-', icon: 'mdi mdi-pound-box-outline' },
{ label: '报销类型', value: selectedRow.value?.type ?? '-', icon: 'mdi mdi-briefcase-outline' },
...summaryItems
])
// 根据搜索关键词筛选
if (listKeyword.value.trim()) {
const keyword = listKeyword.value.trim().toLowerCase()
filteredRows = filteredRows.filter((row) => {
return (
String(row.id || '').toLowerCase().includes(keyword) ||
String(row.applicant || '').toLowerCase().includes(keyword) ||
String(row.department || '').toLowerCase().includes(keyword) ||
String(row.type || '').toLowerCase().includes(keyword) ||
String(row.amount || '').toLowerCase().includes(keyword)
)
})
}
return filteredRows
})
const showTable = computed(() => !loading.value && !error.value && visibleRows.value.length > 0)
const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0)
const approvalEmptyState = computed(() => {
if (!rows.value.length) {
return {
eyebrow: '审批中心',
title: '当前没有待审批单据',
desc: '进入直属领导或财务审批节点的报销单会自动汇总到这里,后续可继续处理或跟踪。',
icon: 'mdi mdi-clipboard-check-outline',
actionLabel: null,
actionIcon: null,
tone: 'slate',
artLabel: 'QUEUE',
tips: ['当前仅展示你有权限处理的单据', '高风险和即将超时单据会优先高亮']
}
}
return {
eyebrow: '状态列表为空',
title: `${activeTab.value}”里暂时没有单据`,
desc: activeTab.value === '已处理'
? '当前视图还没有已处理审批数据,可以先回到全部待审继续处理。'
: '可以切换到其他状态查看,或返回全部待审列表继续处理。',
icon: activeTab.value === '已处理' ? 'mdi mdi-archive-clock-outline' : 'mdi mdi-view-list-outline',
actionLabel: '查看全部待审',
actionIcon: 'mdi mdi-format-list-bulleted',
tone: activeTab.value === '已处理' ? 'amber' : 'sky',
artLabel: activeTab.value === '已处理' ? 'DONE' : 'FILTER',
tips: ['分页与表格只在有数据时展示', '空态页面会保留当前页签上下文说明']
}
})
const approvalSteps = computed(() => selectedRow.value?.progressSteps || [])
const summaryItems = computed(() => selectedRow.value?.summaryItems || [])
const heroSummaryItems = computed(() => selectedRow.value?.heroSummaryItems || [])
const expenseItems = computed(() => selectedRow.value?.expenseItems || [])
const expenseTotal = computed(() => selectedRow.value?.amount || formatCurrency(0))
const uploadedExpenseCount = computed(
() => expenseItems.value.filter((item) => Array.isArray(item?.attachments) && item.attachments.length).length
)
const attachments = computed(() => selectedRow.value?.attachments || [])
const riskItems = computed(() => selectedRow.value?.riskItems || [])
const flowItems = computed(() => selectedRow.value?.flowItems || [])
const currentProgressRingMotion = {
initial: {
scale: 1,
opacity: 0.34,
opacity: 0.34
},
enter: {
scale: [1, 1.42, 1.78],
@@ -64,208 +359,68 @@ export default {
repeatType: 'loop',
repeatDelay: 0.85,
ease: 'easeOut',
times: [0, 0.5, 1],
},
},
times: [0, 0.5, 1]
}
}
}
const expenseItems = [
{
id: 'flight-1',
time: '07-10 07:25',
dayLabel: '周三',
name: '机票',
category: '交通',
desc: '北京首都 → 上海虹桥',
detail: 'MU5103 往返经济舱,含行程单',
amount: '¥2,180',
status: '未超标',
tone: 'ok',
attachmentStatus: '已上传',
attachmentTone: 'ok',
attachmentHint: '电子行程单与机票发票齐全',
attachments: ['电子行程单.pdf', '机票发票.pdf'],
riskLabel: '低风险',
riskTone: 'low',
riskText: '票面信息与行程匹配。'
},
{
id: 'taxi-1',
time: '07-10 10:35',
dayLabel: '周三',
name: '出租车',
category: '市内交通',
desc: '虹桥机场 → 静安酒店',
detail: '落地后前往酒店,含过路费',
amount: '¥86',
status: '未超标',
tone: 'ok',
attachmentStatus: '已上传',
attachmentTone: 'ok',
attachmentHint: '已上传 1 张发票',
attachments: ['出租车发票-0710-01.jpg'],
riskLabel: '中风险',
riskTone: 'medium',
riskText: '高峰加价较高,建议顺带核对上车点。'
},
{
id: 'metro-1',
time: '07-10 18:20',
dayLabel: '周三',
name: '地铁',
category: '市内交通',
desc: '静安酒店 → 客户园区',
detail: '2 号线换乘,通勤交通',
amount: '¥12',
status: '未超标',
tone: 'ok',
attachmentStatus: '已上传',
attachmentTone: 'ok',
attachmentHint: '已上传电子票据',
attachments: ['地铁电子票据-0710.png'],
riskLabel: '低风险',
riskTone: 'low',
riskText: '路线与拜访行程一致。'
},
{
id: 'taxi-2',
time: '07-11 08:40',
dayLabel: '周四',
name: '出租车',
category: '市内交通',
desc: '静安酒店 → 客户园区',
detail: '次日早会前往客户现场',
amount: '¥42',
status: '未超标',
tone: 'ok',
attachmentStatus: '未上传',
attachmentTone: 'missing',
attachmentHint: '缺少对应发票',
attachments: [],
riskLabel: '高风险',
riskTone: 'high',
riskText: '票据缺失,当前无法完成交通费核验。'
},
{
id: 'taxi-3',
time: '07-11 20:55',
dayLabel: '周四',
name: '出租车',
category: '返程交通',
desc: '客户园区 → 虹桥机场',
detail: '夜间返程,触发超标校验',
amount: '¥136',
status: '超标 ¥28',
tone: 'bad',
attachmentStatus: '已上传',
attachmentTone: 'ok',
attachmentHint: '已上传 1 张发票',
attachments: ['出租车发票-0711-02.jpg'],
riskLabel: '中风险',
riskTone: 'medium',
riskText: '金额超差旅标准 ¥28需补充业务说明。'
},
{
id: 'hotel-1',
time: '07-10 至 07-11',
dayLabel: '2 晚',
name: '酒店',
category: '住宿',
desc: '上海静安商务酒店',
detail: '标准大床房 2 晚,含早餐',
amount: '¥2,480',
status: '未超标',
tone: 'ok',
attachmentStatus: '部分上传',
attachmentTone: 'partial',
attachmentHint: '发票已上传,入住清单缺失',
attachments: ['酒店发票.jpg'],
riskLabel: '高风险',
riskTone: 'high',
riskText: '缺少入住清单,住宿真实性待补证。'
},
{
id: 'meal-1',
time: '07-10 至 07-11',
dayLabel: '2 天',
name: '餐补',
category: '补贴',
desc: '差旅餐补',
detail: '按差旅制度自动计算',
amount: '¥372',
status: '未超标',
tone: 'ok',
attachmentStatus: '免上传',
attachmentTone: 'neutral',
attachmentHint: '制度型补贴无需票据',
attachments: [],
riskLabel: '低风险',
riskTone: 'low',
riskText: '系统自动核算,无额外异常。'
},
{
id: 'other-1',
time: '07-11 09:10',
dayLabel: '周四',
name: '其他',
category: '杂费',
desc: '行李寄存 / 打印费',
detail: '客户提案资料打印与寄存服务',
amount: '¥1,612',
status: '未超标',
tone: 'ok',
attachmentStatus: '已上传',
attachmentTone: 'ok',
attachmentHint: '已上传 2 份附件',
attachments: ['打印服务发票.jpg', '行李寄存凭证.jpg'],
riskLabel: '低风险',
riskTone: 'low',
riskText: '用途清晰,金额在授权范围内。'
}
]
function showExpenseRisk(item) {
return ['medium', 'high'].includes(String(item?.riskTone || '').trim())
}
const expenseTotal = '¥6,920'
const uploadedExpenseCount = computed(() => expenseItems.filter((item) => item.attachments.length).length)
const showExpenseRisk = (item) => item.riskTone === 'medium' || item.riskTone === 'high'
const toggleExpenseAttachments = (id) => {
function toggleExpenseAttachments(id) {
expandedExpenseId.value = expandedExpenseId.value === id ? null : id
}
const attachments = [
{ name: '机票.pdf', size: '256 KB', icon: 'mdi mdi-file-pdf-box', iconClass: 'pdf' },
{ name: '酒店发票.jpg', size: '412 KB', icon: 'mdi mdi-image', iconClass: 'img' },
{ name: '行程单.pdf', size: '198 KB', icon: 'mdi mdi-file-pdf-box', iconClass: 'pdf' },
{ name: '出租车发票1.jpg', size: '128 KB', icon: 'mdi mdi-image', iconClass: 'img' },
{ name: '出租车发票2.jpg', size: '132 KB', icon: 'mdi mdi-image', iconClass: 'img' },
{ name: '酒店入住清单', size: '缺失', icon: 'mdi mdi-minus-circle', iconClass: 'miss', missing: true }
]
function handleEmptyAction() {
if (!rows.value.length) {
void reload()
return
}
const riskItems = [
{ text: '酒店入住清单缺失', level: '高', tone: 'high', icon: 'mdi mdi-alert-circle' },
{ text: '1 笔出租车费用超差旅标准 ¥28', level: '中', tone: 'medium', icon: 'mdi mdi-alert' },
{ text: '发票抬头识别为个人,建议核对', level: '中', tone: 'medium', icon: 'mdi mdi-lightbulb-on' }
]
activeTab.value = '全部待审'
}
const flowItems = [
{ label: '提交申请', desc: '刘思雨 提交申请', time: '07-11 08:46', icon: 'mdi mdi-check' },
{ label: '票据识别', desc: 'AI 自动识别完成', time: '07-11 08:48', icon: 'mdi mdi-check' },
{ label: '费用归类', desc: '费用归类完成', time: '07-11 08:49', icon: 'mdi mdi-check' },
{ label: '部门负责人审批', desc: '李文静 已通过', time: '07-11 11:28', icon: 'mdi mdi-check' },
{ label: '财务审批', desc: '张晓明 审批中', time: '进行中', icon: 'mdi mdi-circle-slice-8', current: true },
{ label: '归档入账', desc: '待处理', time: '-', icon: 'mdi mdi-circle-outline', pending: true }
]
async function reload() {
loading.value = true
error.value = ''
try {
const payload = await fetchExpenseClaims()
const mappedRows = Array.isArray(payload)
? payload
.map((item) => mapExpenseClaimToRequest(item))
.filter((item) => item.approvalKey === 'in_progress')
.filter((item) => canCurrentUserProcessRequest(item, currentUser.value))
.map((item) => buildApprovalRow(item))
: []
rows.value = mappedRows
if (!mappedRows.some((item) => item.claimId === selectedClaimId.value)) {
selectedClaimId.value = ''
}
} catch (nextError) {
rows.value = []
selectedClaimId.value = ''
error.value = nextError instanceof Error ? nextError.message : '审批中心加载失败。'
} finally {
loading.value = false
}
}
void reload()
return {
activeTab,
selectedRow,
expandedExpenseId,
listKeyword,
tabs,
filters,
rows,
visibleRows,
showTable,
showEmpty,
approvalEmptyState,
approvalSteps,
summaryItems,
heroSummaryItems,
@@ -277,8 +432,11 @@ export default {
toggleExpenseAttachments,
attachments,
riskItems,
flowItems
flowItems,
handleEmptyAction,
loading,
error,
reload
}
}
}