refactor: consolidate finance workflow modules
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
@new-chat="openAiSidebarNewChat"
|
||||
@open-recent="openAiSidebarRecent"
|
||||
@rename-conversation="handleAiConversationRename"
|
||||
@prefetch-view="prefetchAppView"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
<SidebarRail
|
||||
@@ -49,6 +50,7 @@
|
||||
@logout="handleLogout"
|
||||
@toggle-collapse="toggleSidebarCollapsed"
|
||||
@navigate="handleNavigateWithMobileClose"
|
||||
@prefetch-view="prefetchAppView"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -243,24 +245,18 @@ import AiSidebarRail from '../components/layout/AiSidebarRail.vue'
|
||||
import SidebarRail from '../components/layout/SidebarRail.vue'
|
||||
import TopBar from '../components/layout/TopBar.vue'
|
||||
import FilterBar from '../components/layout/FilterBar.vue'
|
||||
import AuditView from './AuditView.vue'
|
||||
import BudgetCenterView from './BudgetCenterView.vue'
|
||||
import DigitalEmployeesView from './DigitalEmployeesView.vue'
|
||||
import DocumentsCenterView from './DocumentsCenterView.vue'
|
||||
import EmployeeManagementView from './EmployeeManagementView.vue'
|
||||
import OverviewView from './OverviewView.vue'
|
||||
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
|
||||
import PoliciesView from './PoliciesView.vue'
|
||||
import ReceiptFolderView from './ReceiptFolderView.vue'
|
||||
import SettingsView from './SettingsView.vue'
|
||||
import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue'
|
||||
import TravelRequestDetailView from './TravelRequestDetailView.vue'
|
||||
|
||||
import { useAppShell } from '../composables/useAppShell.js'
|
||||
import { useSystemState } from '../composables/useSystemState.js'
|
||||
import { filterNavItemsByAccess, isPlatformAdminUser } from '../utils/accessControl.js'
|
||||
import { isBusinessDocumentReference } from '../utils/aiDocumentDetailReference.js'
|
||||
import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../utils/aiWorkbenchConversationStore.js'
|
||||
import {
|
||||
defineAsyncModalView,
|
||||
defineAsyncRouteView,
|
||||
preloadAppView,
|
||||
scheduleRelatedAppViewPreload
|
||||
} from './scripts/appShellAsyncViews.js'
|
||||
|
||||
const employeeSummary = ref(null)
|
||||
const knowledgeSummary = ref(null)
|
||||
@@ -300,6 +296,18 @@ const aiSidebarCommandSeq = ref(0)
|
||||
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
|
||||
const aiActiveConversationId = ref('')
|
||||
const aiConversationHistory = ref([])
|
||||
const AuditView = defineAsyncRouteView('audit')
|
||||
const BudgetCenterView = defineAsyncRouteView('budget')
|
||||
const DigitalEmployeesView = defineAsyncRouteView('digitalEmployees')
|
||||
const DocumentsCenterView = defineAsyncRouteView('documents')
|
||||
const EmployeeManagementView = defineAsyncRouteView('employees')
|
||||
const OverviewView = defineAsyncRouteView('overview')
|
||||
const PersonalWorkbenchView = defineAsyncRouteView('workbench')
|
||||
const PoliciesView = defineAsyncRouteView('policies')
|
||||
const ReceiptFolderView = defineAsyncRouteView('receiptFolder')
|
||||
const SettingsView = defineAsyncRouteView('settings')
|
||||
const TravelReimbursementCreateView = defineAsyncModalView('travelCreate')
|
||||
const TravelRequestDetailView = defineAsyncRouteView('travelDetail')
|
||||
|
||||
function toggleSidebarCollapsed() {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
@@ -310,6 +318,10 @@ function handleNavigateWithMobileClose(viewId) {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
function prefetchAppView(viewId) {
|
||||
void preloadAppView(viewId).catch(() => {})
|
||||
}
|
||||
|
||||
function toggleWorkbenchMode() {
|
||||
const nextMode = workbenchMode.value === 'ai' ? 'traditional' : 'ai'
|
||||
if (nextMode === 'ai') {
|
||||
@@ -580,4 +592,12 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => activeView.value,
|
||||
(view) => {
|
||||
scheduleRelatedAppViewPreload(view)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
86
web/src/views/scripts/appShellAsyncViews.js
Normal file
86
web/src/views/scripts/appShellAsyncViews.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import AppModalLoadingState from '../../components/shared/AppModalLoadingState.vue'
|
||||
import AppViewLoadingState from '../../components/shared/AppViewLoadingState.vue'
|
||||
|
||||
const appViewLoaders = {
|
||||
audit: () => import('../AuditView.vue'),
|
||||
budget: () => import('../BudgetCenterView.vue'),
|
||||
digitalEmployees: () => import('../DigitalEmployeesView.vue'),
|
||||
documents: () => import('../DocumentsCenterView.vue'),
|
||||
employees: () => import('../EmployeeManagementView.vue'),
|
||||
overview: () => import('../OverviewView.vue'),
|
||||
policies: () => import('../PoliciesView.vue'),
|
||||
receiptFolder: () => import('../ReceiptFolderView.vue'),
|
||||
settings: () => import('../SettingsView.vue'),
|
||||
travelCreate: () => import('../TravelReimbursementCreateView.vue'),
|
||||
travelDetail: () => import('../TravelRequestDetailView.vue'),
|
||||
workbench: () => import('../PersonalWorkbenchView.vue')
|
||||
}
|
||||
|
||||
const appViewPreloadCache = new Map()
|
||||
|
||||
const relatedPreloadViews = {
|
||||
audit: ['documents', 'policies'],
|
||||
budget: ['documents', 'overview'],
|
||||
digitalEmployees: ['overview', 'settings'],
|
||||
documents: ['travelDetail', 'workbench'],
|
||||
employees: ['settings', 'overview'],
|
||||
overview: ['workbench', 'documents'],
|
||||
policies: ['audit', 'documents'],
|
||||
receiptFolder: ['documents', 'workbench'],
|
||||
settings: ['digitalEmployees', 'employees'],
|
||||
workbench: ['documents', 'receiptFolder']
|
||||
}
|
||||
|
||||
export function preloadAppView(viewId) {
|
||||
const normalizedViewId = String(viewId || '').trim()
|
||||
const loader = appViewLoaders[normalizedViewId]
|
||||
if (!loader) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (!appViewPreloadCache.has(normalizedViewId)) {
|
||||
appViewPreloadCache.set(
|
||||
normalizedViewId,
|
||||
loader().catch((error) => {
|
||||
appViewPreloadCache.delete(normalizedViewId)
|
||||
throw error
|
||||
})
|
||||
)
|
||||
}
|
||||
return appViewPreloadCache.get(normalizedViewId)
|
||||
}
|
||||
|
||||
export function defineAsyncRouteView(viewId, options = {}) {
|
||||
return defineAsyncComponent({
|
||||
loader: () => preloadAppView(viewId),
|
||||
loadingComponent: options.loadingComponent || AppViewLoadingState,
|
||||
delay: options.delay ?? 160,
|
||||
timeout: options.timeout ?? 30000,
|
||||
suspensible: false
|
||||
})
|
||||
}
|
||||
|
||||
export function defineAsyncModalView(viewId) {
|
||||
return defineAsyncRouteView(viewId, {
|
||||
loadingComponent: AppModalLoadingState,
|
||||
delay: 80
|
||||
})
|
||||
}
|
||||
|
||||
export function scheduleRelatedAppViewPreload(viewId) {
|
||||
const views = relatedPreloadViews[String(viewId || '').trim()] || []
|
||||
if (!views.length || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const runPreload = () => {
|
||||
for (const view of views.slice(0, 2)) {
|
||||
void preloadAppView(view).catch(() => {})
|
||||
}
|
||||
}
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
window.requestIdleCallback(runPreload, { timeout: 1600 })
|
||||
return
|
||||
}
|
||||
window.setTimeout(runPreload, 360)
|
||||
}
|
||||
@@ -1,319 +1,14 @@
|
||||
import {
|
||||
DATE_INPUT_FORMAT,
|
||||
buildReviewAttachmentStatus,
|
||||
cloneReviewEditFields,
|
||||
createEmptyInlineReviewState,
|
||||
formatAmountDisplay,
|
||||
formatReviewSceneDisplayValue,
|
||||
normalizeReviewRiskLevel,
|
||||
shouldShowReviewFactCard,
|
||||
buildBusinessTimeContextFromReviewValues as buildBusinessTimeContextFromReviewValuesModel,
|
||||
buildReviewFormContextFromPayload as buildReviewFormContextFromPayloadModel,
|
||||
isTravelReviewPayload as isTravelReviewPayloadModel,
|
||||
resolveReviewTravelTransportType as resolveReviewTravelTransportTypeModel
|
||||
} from './travelReimbursementReviewModel.js'
|
||||
|
||||
const REVIEW_PANEL_SCOPE_OVERVIEW = 'overview'
|
||||
const REVIEW_PANEL_SCOPE_DOCUMENTS = 'documents'
|
||||
const REVIEW_PANEL_SCOPE_RISK = 'risk'
|
||||
const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = ['历史报销画像', '用户画像', '制度注意事项', '制度注意']
|
||||
const REVIEW_PENDING_SUMMARY_PATTERN = /(^|\n)\s*(?:当前还有|我这边看到还有|下方还有|这笔报销还有|目前还有|还有|这次识别结果里还有|我还需要你确认|当前信息还差|本次报销还有)\s+[^\n]*(?:信息待补充|风险提醒|细节还需要进一步确认)[^\n]*(?:草稿)[^\n]*。\s*/g
|
||||
|
||||
const REVIEW_RISK_LEVEL_META = {
|
||||
high: {
|
||||
label: '高风险',
|
||||
icon: 'mdi mdi-alert-octagon-outline',
|
||||
suggestion: '建议先处理该项,再提交审批;如果属于合理业务场景,请补充说明或附件。'
|
||||
},
|
||||
medium: {
|
||||
label: '中风险',
|
||||
icon: 'mdi mdi-alert-circle-outline',
|
||||
suggestion: '提交前建议核对业务真实性、附件完整性和规则口径。'
|
||||
},
|
||||
info: {
|
||||
label: '提示',
|
||||
icon: 'mdi mdi-information-outline',
|
||||
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
||||
},
|
||||
low: {
|
||||
label: '低风险',
|
||||
icon: 'mdi mdi-information-outline',
|
||||
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeReviewPanelScope(scope) {
|
||||
const normalized = String(scope || '').trim()
|
||||
return [REVIEW_PANEL_SCOPE_OVERVIEW, REVIEW_PANEL_SCOPE_DOCUMENTS, REVIEW_PANEL_SCOPE_RISK].includes(normalized)
|
||||
? normalized
|
||||
: ''
|
||||
}
|
||||
|
||||
export function canExposeReviewPanelScope(scope) {
|
||||
return Boolean(normalizeReviewPanelScope(scope))
|
||||
}
|
||||
|
||||
export function buildBusinessTimeContextFromReviewValues(values = {}) {
|
||||
return buildBusinessTimeContextFromReviewValuesModel(values)
|
||||
}
|
||||
|
||||
export function buildReviewFormContextFromPayload(reviewPayload, inlineState = null) {
|
||||
return buildReviewFormContextFromPayloadModel(reviewPayload, inlineState)
|
||||
}
|
||||
|
||||
export function buildReviewCorrectionMessage(fields) {
|
||||
const lines = ['请按以下核对后的报销信息更新当前识别结果:']
|
||||
for (const item of cloneReviewEditFields(fields)) {
|
||||
if (!item.label || (!item.value && !item.required)) {
|
||||
continue
|
||||
}
|
||||
lines.push(`${item.label}:${String(item.value || '').trim() || '待补充'}`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function isTravelReviewPayload(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
||||
return isTravelReviewPayloadModel(reviewPayload, inlineState)
|
||||
}
|
||||
|
||||
export function resolveReviewTravelTransportType(reviewPayload, fallbackText = '') {
|
||||
return resolveReviewTravelTransportTypeModel(reviewPayload, fallbackText)
|
||||
}
|
||||
|
||||
export function resolveReviewRiskBriefs(reviewPayload) {
|
||||
if (!Array.isArray(reviewPayload?.risk_briefs)) return []
|
||||
return reviewPayload.risk_briefs.filter((item) => {
|
||||
const title = String(item?.title || '').trim()
|
||||
return !DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS.some((keyword) => title.includes(keyword))
|
||||
})
|
||||
}
|
||||
|
||||
export function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
||||
const pendingAttachmentCount = Math.max(0, Number(inlineState.pending_attachment_count || 0))
|
||||
const totalAttachmentCount = Math.max(0, Number(inlineState.attachment_count || 0))
|
||||
const existingAttachmentCount = Math.max(0, totalAttachmentCount - pendingAttachmentCount)
|
||||
const attachmentStatus =
|
||||
pendingAttachmentCount > 0
|
||||
? existingAttachmentCount > 0
|
||||
? `已上传 ${existingAttachmentCount} 份,待新增 ${pendingAttachmentCount} 份`
|
||||
: `待保存 ${pendingAttachmentCount} 份`
|
||||
: totalAttachmentCount > 0
|
||||
? `已上传 ${totalAttachmentCount} 份`
|
||||
: buildReviewAttachmentStatus(reviewPayload)
|
||||
if (isTravelReviewPayload(reviewPayload, inlineState)) {
|
||||
return [
|
||||
{
|
||||
key: 'occurred_date',
|
||||
label: '发生时间',
|
||||
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-calendar-month-outline',
|
||||
editor: 'date',
|
||||
modelKey: 'occurred_date',
|
||||
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: '金额',
|
||||
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
||||
icon: 'mdi mdi-cash',
|
||||
editor: 'amount',
|
||||
modelKey: 'amount',
|
||||
placeholder: '例如 200.00'
|
||||
},
|
||||
{
|
||||
key: 'transport_type',
|
||||
label: '交通类型',
|
||||
value: String(inlineState.transport_type || '').trim() || '待确认',
|
||||
icon: 'mdi mdi-train-car',
|
||||
editor: 'text',
|
||||
modelKey: 'transport_type',
|
||||
placeholder: '例如 火车/高铁、飞机'
|
||||
},
|
||||
{
|
||||
key: 'hotel_name',
|
||||
label: '酒店名称',
|
||||
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-bed-outline',
|
||||
editor: 'text',
|
||||
modelKey: 'merchant_name',
|
||||
placeholder: '请输入酒店名称'
|
||||
},
|
||||
{
|
||||
key: 'travel_purpose',
|
||||
label: '出差事宜',
|
||||
value: String(inlineState.reason_value || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-briefcase-edit-outline',
|
||||
editor: 'textarea',
|
||||
modelKey: 'reason_value',
|
||||
placeholder: '请填写本次出差的具体工作内容或业务意图',
|
||||
wide: true
|
||||
}
|
||||
]
|
||||
}
|
||||
const cards = [
|
||||
{
|
||||
key: 'occurred_date',
|
||||
label: '发生时间',
|
||||
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-calendar-month-outline',
|
||||
editor: 'date',
|
||||
modelKey: 'occurred_date',
|
||||
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: '金额',
|
||||
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
||||
icon: 'mdi mdi-cash',
|
||||
editor: 'amount',
|
||||
modelKey: 'amount',
|
||||
placeholder: '例如 200.00'
|
||||
},
|
||||
{
|
||||
key: 'scene',
|
||||
label: '场景 / 事由',
|
||||
value: formatReviewSceneDisplayValue(inlineState),
|
||||
icon: 'mdi mdi-silverware-fork-knife',
|
||||
editor: 'select',
|
||||
modelKey: 'scene_label',
|
||||
placeholder: '请选择场景'
|
||||
},
|
||||
{
|
||||
key: 'attachments',
|
||||
label: '票据状态',
|
||||
value: attachmentStatus,
|
||||
icon: 'mdi mdi-file-document-outline',
|
||||
editor: 'upload',
|
||||
modelKey: 'attachment_names',
|
||||
placeholder: ''
|
||||
}
|
||||
]
|
||||
|
||||
if (shouldShowReviewFactCard(reviewPayload, 'customer_name', inlineState.customer_name)) {
|
||||
cards.splice(cards.length - 1, 0, {
|
||||
key: 'customer_name',
|
||||
label: '关联客户',
|
||||
value: String(inlineState.customer_name || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-domain',
|
||||
editor: 'text',
|
||||
modelKey: 'customer_name',
|
||||
placeholder: '请输入客户名称'
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldShowReviewFactCard(reviewPayload, 'location', inlineState.location)) {
|
||||
cards.splice(cards.length - 1, 0, {
|
||||
key: 'location',
|
||||
label: '业务地点',
|
||||
value: String(inlineState.location || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-map-marker-outline',
|
||||
editor: 'text',
|
||||
modelKey: 'location',
|
||||
placeholder: '请输入业务地点'
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldShowReviewFactCard(reviewPayload, 'merchant_name', inlineState.merchant_name)) {
|
||||
cards.splice(cards.length - 1, 0, {
|
||||
key: 'merchant_name',
|
||||
label: '酒店/商户',
|
||||
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-storefront-outline',
|
||||
editor: 'text',
|
||||
modelKey: 'merchant_name',
|
||||
placeholder: '请输入酒店或商户名称'
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldShowReviewFactCard(reviewPayload, 'participants', inlineState.participants)) {
|
||||
cards.splice(cards.length - 1, 0, {
|
||||
key: 'participants',
|
||||
label: '同行人员',
|
||||
value: String(inlineState.participants || '').trim() || '待补充',
|
||||
icon: 'mdi mdi-account-group-outline',
|
||||
editor: 'text',
|
||||
modelKey: 'participants',
|
||||
placeholder: '例如 客户 2 人,我方 1 人'
|
||||
})
|
||||
}
|
||||
|
||||
return cards
|
||||
}
|
||||
|
||||
function normalizeReviewRiskTitle(title, fallbackTitle) {
|
||||
const normalized = String(title || '').trim()
|
||||
const fallback = String(fallbackTitle || '风险提示').trim() || '风险提示'
|
||||
if (!normalized) return fallback
|
||||
const cleaned = normalized
|
||||
.replace(/AI\s*预审\s*(暂未通过|未通过|不通过)?/g, '风险提示')
|
||||
.replace(/(高风险|中风险|低风险)/g, '')
|
||||
.replace(/^[::\-—\s]+|[::\-—\s]+$/g, '')
|
||||
.trim()
|
||||
return cleaned || fallback
|
||||
}
|
||||
|
||||
export function buildReviewRiskItems(reviewPayload) {
|
||||
return resolveReviewRiskBriefs(reviewPayload)
|
||||
.map((brief, index) => {
|
||||
const title = String(brief?.title || '').trim()
|
||||
const content = String(brief?.content || '').trim()
|
||||
const detail = String(brief?.detail || '').trim()
|
||||
const suggestion = String(brief?.suggestion || '').trim()
|
||||
const level = normalizeReviewRiskLevel(brief?.level)
|
||||
const meta = REVIEW_RISK_LEVEL_META[level] || REVIEW_RISK_LEVEL_META.low
|
||||
const fallbackTitle = content ? `风险提示 ${index + 1}` : '风险提示'
|
||||
const normalizedTitle = normalizeReviewRiskTitle(title, fallbackTitle)
|
||||
const summary = content || normalizedTitle
|
||||
|
||||
if (!normalizedTitle && !summary) return null
|
||||
|
||||
return {
|
||||
key: `${level}-${normalizedTitle}-${index}`,
|
||||
title: normalizedTitle,
|
||||
summary,
|
||||
detail: detail || content || '当前风险项没有返回更长解释,建议结合票据、报销事由和规则要求进行复核。',
|
||||
level,
|
||||
levelLabel: meta.label,
|
||||
icon: meta.icon,
|
||||
sourceLabel: meta.label,
|
||||
suggestion: suggestion || meta.suggestion
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function buildReviewRiskConversationText(item, detailTarget = {}) {
|
||||
const title = String(item?.title || '风险提示').trim()
|
||||
const summary = String(item?.summary || '').trim()
|
||||
const detail = String(item?.detail || '').trim()
|
||||
const suggestion = String(item?.suggestion || '').trim()
|
||||
const isInfo = String(item?.level || '').trim() === 'info'
|
||||
const detailHref = String(detailTarget?.href || '').trim()
|
||||
const detailLabel = String(detailTarget?.label || '').trim() || '进入该单据详情重新填写'
|
||||
const lines = [`${title}`]
|
||||
|
||||
if (summary) {
|
||||
lines.push('', `${isInfo ? '提示内容' : '风险点'}:${summary}`)
|
||||
}
|
||||
if (detail && detail !== summary) {
|
||||
lines.push('', `规则依据:${detail}`)
|
||||
}
|
||||
if (suggestion) {
|
||||
lines.push('', `${isInfo ? '处理建议' : '修改建议'}:${suggestion}`)
|
||||
}
|
||||
if (detailHref) {
|
||||
lines.push('', `[${detailLabel}](${detailHref})`)
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function buildReviewMainMessageText(message) {
|
||||
const text = String(message?.text || '')
|
||||
if (!message?.reviewPayload) {
|
||||
return text
|
||||
}
|
||||
return text
|
||||
.replace(REVIEW_PENDING_SUMMARY_PATTERN, '\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
export {
|
||||
buildBusinessTimeContextFromReviewValues,
|
||||
buildReviewCorrectionMessage,
|
||||
buildReviewFactCards,
|
||||
buildReviewFormContextFromPayload,
|
||||
buildReviewMainMessageText,
|
||||
buildReviewRiskConversationText,
|
||||
buildReviewRiskItems,
|
||||
canExposeReviewPanelScope,
|
||||
isTravelReviewPayload,
|
||||
normalizeReviewPanelScope,
|
||||
resolveReviewRiskBriefs,
|
||||
resolveReviewTravelTransportType
|
||||
} from './travelReimbursementReviewPanelModel.js'
|
||||
|
||||
Reference in New Issue
Block a user