feat: 数字员工财务报告体系与定时提醒及看板快照调度

- 新增数字员工财务报告生成、邮件投递与渲染调度器
- 引入员工画像扫描调度与定时提醒任务
- 完善财务看板快照、排行口径与部门人员占比计算
- 优化数字员工工作看板仪表盘与技能目录
- 增强前端总览页图表、工作台摘要与顶部导航栏交互
- 新增差旅申请规划推动提醒与报销创建会话状态管理
- 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 09:25:23 +08:00
parent 0c74b4ab4a
commit 15006a05a7
114 changed files with 7356 additions and 650 deletions

View File

@@ -71,6 +71,7 @@
:knowledge-summary="knowledgeSummary"
:request-summary="requestSummary"
:document-summary="documentSummary"
:workbench-summary="workbenchSummary"
:digital-employee-summary="digitalEmployeeSummary"
:company-name="ENTERPRISE_DISPLAY_NAME"
:detail-mode="resolvedDetailMode"
@@ -84,6 +85,7 @@
@update:overview-dashboard="overviewDashboard = $event"
@batch-approve="toast('已批量通过 23 条审批任务')"
@new-application="openExpenseApplicationCreate"
@open-document="openWorkbenchDocument"
/>
<FilterBar
@@ -124,6 +126,7 @@
:assistant-modal-open="smartEntryOpen"
:workbench-summary="workbenchSummary"
@open-assistant="openSmartEntry"
@open-document="openWorkbenchDocument"
/>
<TravelRequestDetailView
@@ -142,6 +145,7 @@
:has-data="requests.length > 0"
:loading="requestsLoading"
:error="requestsError"
:refresh-token="documentCenterRefreshToken"
@open-document="openRequestDetail"
@create-request="openTravelCreate"
@create-application="openExpenseApplicationCreate"
@@ -188,6 +192,8 @@
:initial-conversation="smartEntryContext.conversation"
:initial-session-type="smartEntryContext.sessionType"
:initial-budget-context="smartEntryContext.budgetContext"
:initial-prompt-auto-submit="smartEntryContext.initialPromptAutoSubmit"
:initial-application-preview="smartEntryContext.initialApplicationPreview"
:entry-source="smartEntryContext.source"
:request-context="smartEntryContext.request"
:invalidated-draft-claim-id="smartEntryInvalidatedDraftClaimId"
@@ -274,6 +280,7 @@ const {
customRange,
detailAlerts,
detailMode,
documentCenterRefreshToken,
filteredRequests,
filters,
handleApprove,
@@ -292,6 +299,7 @@ const {
workbenchSummary,
requestsError,
requestsLoading,
reloadDocumentCenterRequests,
reloadRequests,
requests,
search,
@@ -351,6 +359,20 @@ const resolvedDetailKpis = computed(() => (
customDetailTopBarActive.value ? detailTopBarPayload.value?.kpis || [] : []
))
function openWorkbenchDocument(payload = {}) {
const requestId = String(payload.claimId || payload.id || payload.claimNo || '').trim()
if (!requestId) {
return
}
const request = requests.value.find((item) => (
String(item.claimId || '').trim() === requestId
|| String(item.id || '').trim() === requestId
|| String(item.claimNo || '').trim() === requestId
))
openRequestDetail(request || payload)
}
function handleLogout() {
logout('manual')
}

View File

@@ -196,35 +196,16 @@
</table>
</div>
<footer v-if="showTable" class="list-foot">
<span class="page-summary">{{ pageSummary }}</span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="budgetPage === 1" aria-label="上一页" @click="goToBudgetPage(budgetPage - 1)">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in budgetPageNumbers"
:key="page"
class="page-number"
:class="{ active: budgetPage === page }"
type="button"
:aria-current="budgetPage === page ? 'page' : undefined"
@click="goToBudgetPage(page)"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="budgetPage === totalBudgetPages" aria-label="下一页" @click="goToBudgetPage(budgetPage + 1)">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect
v-model="budgetPageSize"
class="page-size-select"
:options="budgetPageSizeOptions"
size="small"
@change="changeBudgetPageSize"
/>
</footer>
<EnterprisePagination
v-if="showTable"
:current-page="budgetPage"
:page-size="budgetPageSize"
:page-size-options="budgetPageSizeOptions"
:summary="pageSummary"
:total-pages="totalBudgetPages"
@page-size-change="changeBudgetPageSize"
@update:current-page="goToBudgetPage"
/>
</article>
<EnterpriseDetailPage

View File

@@ -154,7 +154,8 @@ import { isPlatformAdminUser } from '../utils/accessControl.js'
import {
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
formatDigitalEmployeeCron,
isDigitalEmployeeAsset
isDigitalEmployeeAsset,
shouldDisplayDigitalEmployeeAsset
} from './scripts/auditViewDigitalEmployeeModel.js'
import {
buildDigitalEmployeeScheduleConfig,
@@ -336,7 +337,9 @@ async function loadEmployees() {
try {
const payload = await fetchAgentAssets({ assetType: 'task' })
const items = Array.isArray(payload)
? payload.filter(isDigitalEmployeeAsset).map(buildEmployeeListItem)
? payload
.filter((asset) => isDigitalEmployeeAsset(asset) && shouldDisplayDigitalEmployeeAsset(asset))
.map(buildEmployeeListItem)
: []
employees.value = sortEmployees(items)

View File

@@ -215,36 +215,23 @@
</table>
</div>
<footer v-if="showTable" class="list-foot">
<span class="page-summary"> {{ filteredRows.length }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect v-model="pageSize" class="page-size-select" :options="pageSizeOptions" size="small" @change="changePageSize" />
</footer>
<EnterprisePagination
v-if="showTable"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:summary="pageSummary"
:total-pages="totalPages"
@page-size-change="changePageSize"
@update:current-page="currentPage = $event"
/>
</article>
</section>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
import EnterprisePagination from '../components/shared/EnterprisePagination.vue'
import TableEmptyState from '../components/shared/TableEmptyState.vue'
import TableLoadingState from '../components/shared/TableLoadingState.vue'
import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js'
@@ -318,7 +305,8 @@ const props = defineProps({
filteredRequests: { type: Array, required: true },
hasData: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
error: { type: String, default: '' }
error: { type: String, default: '' },
refreshToken: { type: Number, default: 0 }
})
const emit = defineEmits([
'open-document',
@@ -463,6 +451,7 @@ const filteredRows = computed(() => {
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value)))
const pageSummary = computed(() => `${filteredRows.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value}`)
const visibleRows = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredRows.value.slice(start, start + pageSize.value)
@@ -832,6 +821,15 @@ watch(documentSummary, (summary) => {
onMounted(() => {
void loadSupportingRows()
})
watch(
() => props.refreshToken,
(token, previousToken) => {
if (token && token !== previousToken) {
void loadSupportingRows()
}
}
)
</script>
<style scoped src="../assets/styles/components/document-list-shared.css"></style>

View File

@@ -657,47 +657,16 @@
</table>
</div>
<footer v-if="!loading && !errorMessage && totalCount" class="list-foot">
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button
class="page-nav"
type="button"
:disabled="currentPage === 1"
aria-label="上一页"
@click="currentPage--"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
<button
class="page-nav"
type="button"
:disabled="currentPage === totalPages"
aria-label="下一页"
@click="currentPage++"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect
v-model="pageSize"
class="page-size-select"
:options="pageSizeOptions"
size="small"
@change="changePageSize"
/>
</footer>
<EnterprisePagination
v-if="!loading && !errorMessage && totalCount"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:summary="pageSummary"
:total-pages="totalPages"
@page-size-change="changePageSize"
@update:current-page="currentPage = $event"
/>
</article>
</Transition>

View File

@@ -14,9 +14,8 @@
:empty="!systemLogLoading && !visibleSystemLogEntries.length"
:total="totalCount"
:total-pages="totalPages"
:pages="visiblePageItems"
:page-size-options="pageSizeOptions"
:summary="`共 ${totalCount} 条系统日志,当前第 ${currentPage} 页`"
:summary="`共 ${totalCount} 条系统日志,当前第 ${currentPage} / ${totalPages} 页`"
:show-pagination="!systemLogLoading && filteredSystemLogEntries.length > 0"
loading-title="系统日志同步中"
loading-message="正在加载系统运行日志记录"

View File

@@ -69,7 +69,7 @@
<div class="content-grid bottom-grid">
<article class="panel dashboard-card rank-panel">
<div class="card-head">
<h3>部门报销排行费用金额<i class="mdi mdi-information-outline"></i></h3>
<h3>部门报销排行 <i class="mdi mdi-information-outline"></i></h3>
<EnterpriseSelect
v-model="activeDepartmentRange"
class="card-select"
@@ -84,7 +84,14 @@
<article class="panel dashboard-card employee-rank-panel">
<div class="card-head">
<h3>个人报销排行本月<i class="mdi mdi-information-outline"></i></h3>
<h3>个人报销排行 <i class="mdi mdi-information-outline"></i></h3>
<EnterpriseSelect
v-model="activeDepartmentRange"
class="card-select"
:options="departmentRangeOptions"
aria-label="个人排行时间范围"
size="small"
/>
</div>
<BarChart :items="rankedEmployees" />
@@ -92,22 +99,33 @@
<article class="panel dashboard-card top-claim-panel">
<div class="card-head">
<h3>本月高额单据 <i class="mdi mdi-information-outline"></i></h3>
<h3>高额单据 <i class="mdi mdi-information-outline"></i></h3>
<span class="card-range-chip">{{ activeDepartmentRange }}</span>
</div>
<div class="top-claim-list">
<div
v-for="item in topClaims"
:key="item.claimNo"
class="top-claim-row"
>
<div>
<strong>{{ item.claimNo }}</strong>
<span>{{ item.employeeName }} · {{ item.departmentName || '未归属部门' }}</span>
</div>
<div>
<strong>{{ item.amountLabel }}</strong>
<span>{{ item.expenseTypeLabel }} · {{ item.statusLabel }}</span>
<div class="top-claim-split">
<div class="department-employee-mix">
<DonutChart
:items="departmentEmployeeLegend"
:center-value="departmentEmployeeCenterValue"
center-label="人员占比"
/>
</div>
<div class="top-claim-list">
<div
v-for="item in topClaims"
:key="item.claimNo"
class="top-claim-row"
>
<div>
<strong>{{ item.claimNo }}</strong>
<span>{{ item.employeeName }} · {{ item.departmentName || '未归属部门' }}</span>
</div>
<div>
<strong>{{ item.amountLabel }}</strong>
<span>{{ item.expenseTypeLabel }} · {{ item.statusLabel }}</span>
</div>
</div>
</div>
</div>
@@ -158,6 +176,7 @@
:dashboard="riskDashboard"
:loading="riskDashboardLoading"
:error="riskDashboardError"
:last-updated-at="riskDashboardLastUpdatedAt"
:level-legend="riskLevelLegend"
:source-legend="riskSourceLegend"
:signal-ranking="riskSignalRanking"
@@ -358,6 +377,8 @@ const {
activeTrendRange,
budgetMetrics,
budgetSummary,
departmentEmployeeCenterValue,
departmentEmployeeLegend,
departmentRangeOptions,
digitalEmployeeCategoryRows,
digitalEmployeeDashboard,
@@ -371,6 +392,7 @@ const {
rankedEmployees,
riskDashboard,
riskDashboardError,
riskDashboardLastUpdatedAt,
riskDashboardLoading,
riskDailyTrendRows,
riskKpiMetrics,

View File

@@ -4,6 +4,7 @@
:assistant-modal-open="assistantModalOpen"
:workbench-summary="workbenchSummary"
@open-assistant="emit('open-assistant', $event)"
@open-document="emit('open-document', $event)"
/>
</template>
@@ -15,5 +16,5 @@ defineProps({
workbenchSummary: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['open-assistant'])
const emit = defineEmits(['open-assistant', 'open-document'])
</script>

View File

@@ -146,35 +146,15 @@
</table>
</div>
<footer class="list-foot">
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect
v-model="pageSize"
class="page-size-select"
:options="pageSizeOptions"
size="small"
@change="changePageSize"
/>
</footer>
<EnterprisePagination
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:summary="pageSummary"
:total-pages="totalPages"
@page-size-change="changePageSize"
@update:current-page="currentPage = $event"
/>
</section>
</div>
</article>

View File

@@ -112,28 +112,16 @@
</table>
</div>
<footer v-if="showTable" class="list-foot">
<span class="page-summary"> {{ filteredRows.length }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<EnterpriseSelect v-model="pageSize" class="page-size-select" :options="pageSizeOptions" size="small" @change="currentPage = 1" />
</footer>
<EnterprisePagination
v-if="showTable"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:summary="pageSummary"
:total-pages="totalPages"
@page-size-change="changePageSize"
@update:current-page="currentPage = $event"
/>
</article>
<EnterpriseDetailPage
@@ -395,7 +383,7 @@ import { ElCheckbox, ElCheckboxGroup } from 'element-plus/es/components/checkbox
import { ElCollapse, ElCollapseItem } from 'element-plus/es/components/collapse/index.mjs'
import { ElDialog } from 'element-plus/es/components/dialog/index.mjs'
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
import EnterprisePagination from '../components/shared/EnterprisePagination.vue'
import EnterpriseDetailCard from '../components/shared/EnterpriseDetailCard.vue'
import EnterpriseDetailPage from '../components/shared/EnterpriseDetailPage.vue'
import TableEmptyState from '../components/shared/TableEmptyState.vue'
@@ -477,6 +465,7 @@ const filteredRows = computed(() => {
].filter(Boolean).join('').toLowerCase().includes(normalized))
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value)))
const pageSummary = computed(() => `${filteredRows.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value}`)
const visibleRows = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredRows.value.slice(start, start + pageSize.value)
@@ -599,6 +588,11 @@ function switchStatus(status) {
activeStatus.value = status
}
function changePageSize(size) {
pageSize.value = Number(size) || pageSize.value
currentPage.value = 1
}
async function reloadReceipts() {
loading.value = true
error.value = ''

View File

@@ -476,6 +476,16 @@
<i class="mdi mdi-trash-can-outline"></i>
{{ deleteBusy ? '删除中' : deleteActionLabel }}
</button>
<button
v-if="canModifyReturnedApplication"
class="secondary-action"
type="button"
:disabled="actionBusy"
@click="handleModifyApplication"
>
<i class="mdi mdi-pencil-outline"></i>
修改申请
</button>
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
<i :class="submitActionIcon"></i>
{{ submitActionLabel }}
@@ -773,3 +783,4 @@
<script src="./scripts/TravelRequestDetailView.js"></script>
<style scoped src="../assets/styles/views/travel-request-detail-view.css"></style>
<style scoped src="../assets/styles/views/travel-request-detail-view-part2.css"></style>
<style scoped src="../assets/styles/views/travel-request-detail-responsive.css"></style>

View File

@@ -4,6 +4,7 @@ import { ElButton } from 'element-plus/es/components/button/index.mjs'
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
import EnterpriseDetailCard from '../../components/shared/EnterpriseDetailCard.vue'
import EnterpriseDetailPage from '../../components/shared/EnterpriseDetailPage.vue'
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
@@ -98,6 +99,7 @@ export default {
emits: ['openAssistant', 'detail-open-change', 'detail-topbar-change'],
components: {
BudgetTrendChart,
EnterprisePagination,
EnterpriseSelect,
EnterpriseDetailCard,
EnterpriseDetailPage,
@@ -169,9 +171,6 @@ export default {
const currentBudgetPage = computed(() =>
Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value)
)
const budgetPageNumbers = computed(() =>
Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1)
)
const visibleBudgetRows = computed(() => {
const pageSize = Number(budgetPageSize.value || 8)
const start = (currentBudgetPage.value - 1) * pageSize
@@ -227,7 +226,7 @@ export default {
artLabel: '预算列表为空',
tips: ['可以调整年度、季度、状态或关键词后重试。']
}))
const pageSummary = computed(() => `${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value}`)
const pageSummary = computed(() => `${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value} / ${totalBudgetPages.value}`)
function buildBudgetAssistantContext(row, mode = 'edit') {
if (!row) return null
@@ -425,7 +424,6 @@ export default {
budgetKeyword,
budgetLoading,
budgetPage: currentBudgetPage,
budgetPageNumbers,
budgetPageSize,
budgetPageSizeOptions,
budgetScopeTabs,

View File

@@ -1,6 +1,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
@@ -452,6 +453,7 @@ export default {
name: 'EmployeeManagementView',
components: {
ConfirmDialog,
EnterprisePagination,
EnterpriseSelect,
TableLoadingState,
TableEmptyState
@@ -672,6 +674,7 @@ export default {
const totalCount = computed(() => filteredEmployees.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
const pageSummary = computed(() => `${totalCount.value} 条,目前第 ${currentPage.value} / ${totalPages.value}`)
const visibleEmployees = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
@@ -1469,6 +1472,7 @@ export default {
hasEmployeeFilters,
totalCount,
totalPages,
pageSummary,
resetFilters,
handleEmployeeEmptyAction,
openEmployeeDetail,

View File

@@ -152,12 +152,6 @@ export default {
filteredSystemLogEntries.value.filter((entry) => entry.level === 'INFO').length
)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
const visiblePageItems = computed(() => {
if (totalPages.value <= 6) {
return Array.from({ length: totalPages.value }, (_, index) => index + 1)
}
return [1, 2, 3, 4, 5, 'ellipsis', totalPages.value]
})
const visibleSystemLogEntries = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredSystemLogEntries.value.slice(start, start + pageSize.value)
@@ -300,7 +294,6 @@ export default {
systemSearchKeyword,
totalCount,
totalPages,
visiblePageItems,
visibleSystemLogEntries
}
}

View File

@@ -1,7 +1,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
@@ -87,7 +87,7 @@ export default {
name: 'PoliciesView',
components: {
ConfirmDialog,
EnterpriseSelect,
EnterprisePagination,
TableLoadingState
},
emits: ['summary-change'],
@@ -182,9 +182,10 @@ export default {
&& activeFolderIngestStats.value.syncing === 0
)
const totalCount = computed(() => filteredDocuments.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
const visibleDocuments = computed(() => {
const totalCount = computed(() => filteredDocuments.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
const pageSummary = computed(() => `${totalCount.value} 条,目前第 ${currentPage.value} / ${totalPages.value}`)
const visibleDocuments = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredDocuments.value.slice(start, start + pageSize.value)
})
@@ -636,6 +637,7 @@ export default {
loading,
pageSize,
pageSizeOptions,
pageSummary,
pageSizes,
onlyOfficeError,
onlyOfficeHostId,

View File

@@ -49,6 +49,13 @@ import {
buildLocalApplicationPreviewMessage,
normalizeApplicationPreview
} from '../../utils/expenseApplicationPreview.js'
import {
TRAVEL_PLANNING_ACTION_GENERATE,
TRAVEL_PLANNING_ACTION_SKIP,
buildTravelPlanningNudgeMessage,
buildTravelPlanningRecommendation,
buildTravelPlanningSuggestedActions
} from '../../utils/travelApplicationPlanning.js'
import {
calculateTravelReimbursement,
createExpenseClaimItem,
@@ -524,6 +531,14 @@ export default {
type: String,
default: ''
},
initialPromptAutoSubmit: {
type: Boolean,
default: true
},
initialApplicationPreview: {
type: Object,
default: null
},
initialFiles: {
type: Array,
default: () => []
@@ -629,7 +644,9 @@ export default {
handleApplicationPreviewEditorKeydown
} = useApplicationPreviewEditor({
persistSessionState,
toast
toast,
calculateTravelReimbursement,
currentUser
})
function applyLinkedApplicationPreviewDateSelection(selection) {
@@ -1372,6 +1389,14 @@ export default {
currentInsight.value =
currentInsight.value
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
meta: ['修改申请'],
applicationPreview
}))
persistSessionState()
}
if (props.initialPrompt?.trim() || props.initialFiles.length) {
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
composerDraft.value = props.initialPrompt.trim()
@@ -1380,7 +1405,12 @@ export default {
if (initialMerge.overflowCount > 0) {
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
}
submitComposer()
nextTick(() => {
adjustComposerTextareaHeight()
})
if (props.initialPromptAutoSubmit !== false) {
submitComposer()
}
}
})
@@ -1576,6 +1606,32 @@ export default {
if (await handleGuidedSuggestedAction(message, action)) return
if (await handleSceneSelectionApplicationGate(message, action)) return
if (actionType === TRAVEL_PLANNING_ACTION_GENERATE) {
if (!lockSuggestedActionMessage(message, action)) return
const sourcePreview = action?.payload?.applicationPreview || action?.payload?.preview || null
const sourceDraftPayload = action?.payload?.draftPayload || action?.payload?.draft_payload || null
const recommendation = buildTravelPlanningRecommendation(sourcePreview, sourceDraftPayload)
if (recommendation) {
messages.value.push(createMessage('user', '生成行程规划'))
messages.value.push(createMessage('assistant', recommendation, [], {
meta: ['行程规划建议']
}))
nextTick(scrollToBottom)
persistSessionState()
}
return
}
if (actionType === TRAVEL_PLANNING_ACTION_SKIP) {
if (!lockSuggestedActionMessage(message, action)) return
messages.value.push(createMessage('assistant', '好的,本次先保留申请结果。后续需要规划交通或酒店时,可以继续在这里告诉我。', [], {
meta: ['暂不规划']
}))
nextTick(scrollToBottom)
persistSessionState()
return
}
if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const targetSessionType = String(actionPayload.session_type || '').trim()
@@ -2033,6 +2089,17 @@ export default {
}
}
function resolveApplicationEditClaimId() {
if (activeSessionType.value !== SESSION_TYPE_APPLICATION) {
return ''
}
const request = linkedRequest.value || {}
if (!request.applicationEditMode) {
return ''
}
return String(request.claimId || request.claim_id || '').trim()
}
async function confirmApplicationSubmit() {
const message = applicationSubmitConfirmDialog.value.message
if (!message || submitting.value || reviewActionBusy.value) {
@@ -2044,6 +2111,7 @@ export default {
const applicationSubmitText = applicationPreview
? buildApplicationPreviewSubmitText(applicationPreview)
: '确认提交'
const applicationEditClaimId = resolveApplicationEditClaimId()
applicationSubmitConfirmDialog.value = {
open: false,
message: null
@@ -2059,7 +2127,16 @@ export default {
feedbackOperationType: 'submit_application',
extraContext: {
application_preview: applicationPreview,
user_input_text: applicationSubmitText
user_input_text: applicationSubmitText,
...(applicationEditClaimId
? {
application_edit_claim_id: applicationEditClaimId,
application_edit_claim_no: String(linkedRequest.value?.claimNo || linkedRequest.value?.id || '').trim(),
application_edit_mode: true,
draft_claim_id: applicationEditClaimId,
selected_claim_id: applicationEditClaimId
}
: {})
}
})
const draftPayload = payload?.result?.draft_payload || {}
@@ -2074,6 +2151,23 @@ export default {
documentType: 'application'
})
}
const planningText = buildTravelPlanningNudgeMessage(applicationPreview, draftPayload)
const planningActions = buildTravelPlanningSuggestedActions(applicationPreview, draftPayload).map((action) => ({
...action,
payload: {
...(action.payload || {}),
applicationPreview,
draftPayload
}
}))
if (planningText && planningActions.length) {
messages.value.push(createMessage('assistant', planningText, [], {
meta: ['行程规划推荐'],
suggestedActions: planningActions
}))
persistSessionState()
nextTick(scrollToBottom)
}
} finally {
reviewActionBusy.value = false
}

View File

@@ -460,11 +460,17 @@ export default {
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
const canOpenAiEntry = computed(() => isEditableRequest.value)
const canModifyReturnedApplication = computed(() => (
isApplicationDocument.value
&& isEditableRequest.value
&& isCurrentApplicant.value
&& String(request.value.status || '').trim().toLowerCase() === 'returned'
))
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
const canDeleteRequest = computed(() => {
if (isApplicationDocument.value) {
return isPlatformAdminUser(currentUser.value)
return isPlatformAdminUser(currentUser.value) || (isEditableRequest.value && isCurrentApplicant.value)
}
if (isArchivedRequest.value) {
return canDeleteArchivedExpenseClaims(currentUser.value)
@@ -1007,7 +1013,7 @@ export default {
if (analysis) {
return {
label: analysis.label || '已上传',
tone: analysis.severity === 'pass' ? 'pass' : analysis.severity || 'low',
tone: normalizeRiskTone(analysis.severity || 'low'),
headline: analysis.headline || 'AI提示',
summary: analysis.summary || '',
points: Array.isArray(analysis.points) ? analysis.points : [],
@@ -1858,7 +1864,9 @@ export default {
toast(
isArchivedRequest.value
? '已归档单据不能删除,只有高级管理员可以执行删除。'
: '当前单据已进入流程,只有高级财务人员可以删除。'
: isApplicationDocument.value
? '当前申请单已进入审批流程,只有退回后申请人本人或系统管理员可以删除。'
: '当前单据已进入流程,只有高级财务人员可以删除。'
)
return
}
@@ -2019,6 +2027,76 @@ export default {
})
}
function buildApplicationEditPreview() {
const factEntries = applicationDetailFactItems.value
.map((item) => [String(item?.label || '').trim(), String(item?.value || '').trim()])
.filter(([label, value]) => label && value)
const facts = new Map(factEntries)
const pickFact = (...labels) => {
for (const label of labels) {
const value = facts.get(label)
if (value) {
return value
}
}
return ''
}
const tripStart = pickFact('出发时间')
const tripReturn = pickFact('返回时间')
const time = tripStart && tripReturn && tripStart !== tripReturn
? `${tripStart}${tripReturn}`
: pickFact('行程时间', '申请时间', '招待时间', '发生时间') || tripStart
return {
sourceText: '修改申请',
modelReviewStatus: 'template',
fields: {
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
grade: pickFact('职级') || request.value.profileGrade || '',
department: request.value.profileDepartment || request.value.departmentName || request.value.department || '',
position: request.value.profilePosition || request.value.employeePosition || request.value.position || '',
managerName: request.value.profileManager || request.value.managerName || request.value.manager || '',
time,
location: pickFact('地点') || request.value.location || request.value.city || '',
reason: pickFact('事由') || request.value.reason || '',
days: pickFact('天数'),
transportMode: pickFact('出行方式'),
lodgingDailyCap: pickFact('住宿上限/天'),
subsidyDailyCap: pickFact('补贴标准/天'),
transportPolicy: pickFact('交通费用口径'),
policyEstimate: pickFact('规则测算参考'),
amount: pickFact('系统预估费用', '用户预估费用', '预计金额') || request.value.amountDisplay || request.value.amount || ''
}
}
}
function handleModifyApplication() {
if (!canModifyReturnedApplication.value) {
return
}
const claimId = String(request.value?.claimId || '').trim()
emit('openAssistant', {
source: 'application',
sessionType: 'application',
prompt: '',
applicationPreview: buildApplicationEditPreview(),
request: {
...request.value,
applicationEditMode: true
},
restoreLatestConversation: false,
initialPromptAutoSubmit: false,
scope: claimId
? {
type: 'claim',
claimId
}
: null
})
}
onBeforeUnmount(() => {
closeAttachmentPreview()
})
@@ -2032,7 +2110,7 @@ export default {
applicationDetailFactItems, relatedApplicationFactItems,
approveBusyText, approveConfirmText, approveConfirmTitle, canDeleteRequest, canManageCurrentClaim,
canNavigateAttachmentPreview,
canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
canModifyReturnedApplication, canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
closeRiskOverrideDialog,
closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
@@ -2046,6 +2124,7 @@ export default {
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
goToNextSubmitRisk, goToPreviousSubmitRisk,
handleAddExpenseItem, handleApproveRequest, handleDeleteRequest, handleExpenseFileChange,
handleModifyApplication,
handlePayRequest,
handleReturnRequest, handleSubmit, heroFactItems, isApplicationDocument, isDraftRequest, isEditableRequest, isTravelRequest,
isMajorExpenseRisk,

View File

@@ -1,10 +1,28 @@
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
export const DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES = new Set([
'finance_dashboard_snapshot',
'digital_employee_reminder_scan',
'employee_behavior_profile_scan',
'department_expense_baseline_accumulate',
'budget_overrun_precontrol_evaluate',
'multi_evidence_consistency_evaluate',
'travel_spatiotemporal_consistency_evaluate',
'global_risk_scan',
'finance_policy_knowledge_organize'
])
const TASK_TYPE_LABELS = {
finance_dashboard_snapshot: '财务经营快照沉淀',
digital_employee_reminder_scan: '定时提醒与待办扫描',
daily_risk_scan: '每日风险巡检',
global_risk_scan: '财务风险图谱巡检',
employee_behavior_profile_scan: '员工行为画像巡检',
department_expense_baseline_accumulate: '部门费用基线沉淀',
budget_overrun_precontrol_evaluate: '预算占用与超标预警',
multi_evidence_consistency_evaluate: '单据多凭证一致性评估',
travel_spatiotemporal_consistency_evaluate: '差旅时空一致性评估',
weekly_ar_summary: '周度应收账龄汇总',
weekly_expense_report: '周度费用洞察',
rule_review_digest: '规则待审摘要',
@@ -15,9 +33,15 @@ const TASK_TYPE_LABELS = {
}
const TASK_TYPE_SKILL_CATEGORIES = {
finance_dashboard_snapshot: '整理',
digital_employee_reminder_scan: '升级',
daily_risk_scan: '评估',
global_risk_scan: '评估',
employee_behavior_profile_scan: '评估',
employee_behavior_profile_scan: '积累',
department_expense_baseline_accumulate: '积累',
budget_overrun_precontrol_evaluate: '评估',
multi_evidence_consistency_evaluate: '评估',
travel_spatiotemporal_consistency_evaluate: '评估',
weekly_ar_summary: '整理',
weekly_expense_report: '整理',
rule_review_digest: '升级',
@@ -145,6 +169,12 @@ export function isDigitalEmployeeAsset(source = {}) {
)
}
export function shouldDisplayDigitalEmployeeAsset(source = {}) {
const content = parseDigitalEmployeeContent(source.current_version_content)
const taskType = resolveDigitalEmployeeTaskType(source, content)
return DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES.has(taskType)
}
export function formatDigitalEmployeeCron(value) {
const raw = normalizeDigitalEmployeeText(value)
if (!raw) {

View File

@@ -18,9 +18,31 @@ const KNOWLEDGE_JOB_TYPES = new Set([
'finance_policy_knowledge_organize'
])
export const VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES = new Set([
'finance_dashboard_snapshot',
'digital_employee_reminder_scan',
'employee_behavior_profile_scan',
'department_expense_baseline_accumulate',
'budget_overrun_precontrol_evaluate',
'multi_evidence_consistency_evaluate',
'travel_spatiotemporal_consistency_evaluate',
'global_risk_scan',
'finance_policy_knowledge_organize'
])
const DAILY_COMPACT_TASK_TYPES = new Set([
'finance_dashboard_snapshot'
])
const TASK_TYPE_LABELS = {
finance_dashboard_snapshot: '财务经营快照沉淀',
digital_employee_reminder_scan: '定时提醒与待办扫描',
global_risk_scan: '财务风险图谱巡检',
employee_behavior_profile_scan: '员工行为画像巡检',
department_expense_baseline_accumulate: '部门费用基线沉淀',
budget_overrun_precontrol_evaluate: '预算占用与超标预警',
multi_evidence_consistency_evaluate: '单据多凭证一致性评估',
travel_spatiotemporal_consistency_evaluate: '差旅时空一致性评估',
risk_clue_collect: '风险线索归集',
finance_policy_knowledge_organize: '知识制度整理',
knowledge_index_sync: '知识制度整理',
@@ -29,10 +51,16 @@ const TASK_TYPE_LABELS = {
}
const TASK_CODE_TO_TYPE = {
'task.hermes.finance_dashboard_snapshot': 'finance_dashboard_snapshot',
'task.hermes.digital_employee_reminder_scan': 'digital_employee_reminder_scan',
'task.hermes.global_risk_scan': 'global_risk_scan',
'task.hermes.employee_behavior_profile_scan': 'employee_behavior_profile_scan',
'task.hermes.risk_rule_discovery': 'risk_clue_collect',
'task.hermes.finance_policy_knowledge_organize': 'finance_policy_knowledge_organize'
'task.hermes.department_expense_baseline_accumulate': 'department_expense_baseline_accumulate',
'task.hermes.budget_overrun_precontrol_evaluate': 'budget_overrun_precontrol_evaluate',
'task.hermes.multi_evidence_consistency_evaluate': 'multi_evidence_consistency_evaluate',
'task.hermes.travel_spatiotemporal_consistency_evaluate': 'travel_spatiotemporal_consistency_evaluate',
'task.hermes.finance_policy_knowledge_organize': 'finance_policy_knowledge_organize',
'task.hermes.risk_rule_discovery': 'risk_clue_collect'
}
function toObject(value) {
@@ -52,6 +80,12 @@ function resolveTaskTypeFromToolName(value) {
if (name.includes('financial_risk_graph')) {
return 'global_risk_scan'
}
if (name.includes('finance_dashboard_snapshot') || name.includes('finance_dashboard')) {
return 'finance_dashboard_snapshot'
}
if (name.includes('digital_employee_reminder') || name.includes('reminder')) {
return 'digital_employee_reminder_scan'
}
if (name.includes('employee_behavior_profile')) {
return 'employee_behavior_profile_scan'
}
@@ -128,6 +162,43 @@ export function resolveWorkRecordTaskType(run) {
return ''
}
export function isVisibleDigitalEmployeeWorkRecord(run) {
const taskType = resolveWorkRecordTaskType(run)
return VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES.has(taskType)
}
function resolveWorkRecordDayKey(run) {
const date = new Date(run?.started_at || run?.finished_at || '')
if (Number.isNaN(date.getTime())) {
return 'unknown'
}
return date.toISOString().slice(0, 10)
}
export function compactDigitalEmployeeWorkRecords(items = []) {
const rows = []
const compactedKeys = new Set()
for (const run of items) {
const taskType = resolveWorkRecordTaskType(run)
if (!VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES.has(taskType)) {
continue
}
if (DAILY_COMPACT_TASK_TYPES.has(taskType)) {
const key = `${taskType}:${resolveWorkRecordDayKey(run)}`
if (compactedKeys.has(key)) {
continue
}
compactedKeys.add(key)
}
rows.push(run)
}
return rows
}
export function resolveWorkRecordTaskLabel(run) {
const taskType = resolveWorkRecordTaskType(run)
return TASK_TYPE_LABELS[taskType] || ''
@@ -135,6 +206,12 @@ export function resolveWorkRecordTaskLabel(run) {
export function resolveWorkRecordProductKind(run) {
const taskType = resolveWorkRecordTaskType(run)
if (taskType === 'finance_dashboard_snapshot') {
return 'finance_snapshot'
}
if (taskType === 'digital_employee_reminder_scan') {
return 'reminder_scan'
}
if (taskType === 'global_risk_scan') {
return 'risk_graph'
}

View File

@@ -458,6 +458,7 @@ export function sanitizeRequest(request) {
const normalized = {
claimId: String(request.claimId || request.claim_id || '').trim(),
claimNo: String(request.claimNo || request.claim_no || request.documentNo || '').trim(),
id: String(request.id || '').trim(),
typeLabel: String(request.typeLabel || request.category || '').trim(),
reason: String(request.reason || request.title || '').trim(),
@@ -468,7 +469,8 @@ export function sanitizeRequest(request) {
amount: String(request.amount || '').trim(),
node: String(request.node || '').trim(),
approval: String(request.approval || '').trim(),
travel: String(request.travel || '').trim()
travel: String(request.travel || '').trim(),
applicationEditMode: Boolean(request.applicationEditMode || request.application_edit_mode)
}
return Object.values(normalized).some(Boolean) ? normalized : null

View File

@@ -150,7 +150,7 @@ function resolveRequestBusinessStage(request = {}) {
function normalizeTone(value) {
const tone = normalizeText(value).toLowerCase()
if (tone === 'pass') return 'pass'
if (['pass', 'success', 'ok', 'normal', 'none', 'compliant', 'approved'].includes(tone)) return 'pass'
if (tone === 'high') return 'high'
if (tone === 'medium') return 'medium'
if (tone === 'low') return 'low'

View File

@@ -2,9 +2,13 @@ import { ref } from 'vue'
import {
APPLICATION_TRANSPORT_MODE_OPTIONS,
applyApplicationPolicyEstimateError,
applyApplicationPolicyEstimateResult,
buildApplicationPreviewRows,
buildApplicationPolicyEstimateRequest,
buildLocalApplicationPreviewMessage,
normalizeApplicationPreview,
resolveApplicationDaysFromDateRange,
refreshApplicationPreviewTransportEstimate
} from '../../utils/expenseApplicationPreview.js'
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
@@ -44,6 +48,27 @@ function shouldRefreshTransportEstimate(fieldKey) {
return ['transportMode', 'time', 'location', 'days'].includes(fieldKey)
}
function resolveEditorCurrentUser(currentUser) {
if (currentUser && typeof currentUser === 'object' && 'value' in currentUser) {
return currentUser.value || {}
}
return currentUser || {}
}
function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue = '') {
const nextFields = {
...fields,
[editor.fieldKey]: nextValue
}
if (editor.fieldKey === 'time') {
const resolvedDays = resolveApplicationDaysFromDateRange(nextValue)
if (resolvedDays) {
nextFields.days = resolvedDays
}
}
return nextFields
}
function buildTransportEstimatePendingPreview(preview = {}) {
const fields = preview?.fields || {}
return normalizeApplicationPreview({
@@ -57,9 +82,29 @@ function buildTransportEstimatePendingPreview(preview = {}) {
})
}
export function useApplicationPreviewEditor({ persistSessionState, toast } = {}) {
export function useApplicationPreviewEditor({
persistSessionState,
toast,
calculateTravelReimbursement,
currentUser
} = {}) {
const applicationPreviewEditor = ref(buildEmptyEditor())
async function refreshApplicationPreviewEstimate(preview = {}) {
const user = resolveEditorCurrentUser(currentUser)
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, user)
if (estimateRequest.canCalculate && typeof calculateTravelReimbursement === 'function') {
try {
const result = await calculateTravelReimbursement(estimateRequest.payload)
return applyApplicationPolicyEstimateResult(preview, result, user)
} catch (error) {
console.warn('Application preview estimate refresh failed:', error)
return applyApplicationPolicyEstimateError(preview, error, user)
}
}
return refreshApplicationPreviewTransportEstimate(preview)
}
function resolveApplicationPreviewRows(message) {
return buildApplicationPreviewRows(message?.applicationPreview || {})
}
@@ -158,25 +203,29 @@ export function useApplicationPreviewEditor({ persistSessionState, toast } = {})
}
const nextPreview = normalizeApplicationPreview({
...message.applicationPreview,
fields: {
...(message.applicationPreview.fields || {}),
[editor.fieldKey]: nextValue
}
fields: buildEditedApplicationPreviewFields(
message.applicationPreview.fields || {},
editor,
nextValue
)
})
const needRefreshTransport = shouldRefreshTransportEstimate(editor.fieldKey) && String(nextPreview.fields?.transportMode || '').trim()
message.applicationPreview = needRefreshTransport
const needRefreshEstimate = shouldRefreshTransportEstimate(editor.fieldKey)
const transportMode = String(nextPreview.fields?.transportMode || '').trim()
message.applicationPreview = needRefreshEstimate
? buildTransportEstimatePendingPreview(nextPreview)
: nextPreview
message.text = buildLocalApplicationPreviewMessage(message.applicationPreview)
cancelApplicationPreviewEditor()
persistSessionState?.()
if (needRefreshTransport) {
await waitForMockApplicationTransportQuote({
transportMode: nextPreview.fields.transportMode,
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
time: nextPreview.fields.time
})
const refreshedPreview = refreshApplicationPreviewTransportEstimate(nextPreview)
if (needRefreshEstimate) {
if (transportMode) {
await waitForMockApplicationTransportQuote({
transportMode,
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
time: nextPreview.fields.time
})
}
const refreshedPreview = await refreshApplicationPreviewEstimate(nextPreview)
message.applicationPreview = refreshedPreview
message.text = buildLocalApplicationPreviewMessage(refreshedPreview)
persistSessionState?.()

View File

@@ -207,6 +207,7 @@ export function useTravelReimbursementSessionState({
shouldPersistLocalSnapshot
&& props.entrySource !== 'budget'
&& !String(props.initialPrompt || '').trim()
&& !props.initialApplicationPreview
&& !props.initialFiles.length
const persistedInitialSnapshot = readAssistantSessionSnapshot(resolveCurrentUserId(), initialSessionType)
const persistedInitialState = canRestorePersistedInitialState