fix(workbench): correct home overview aggregation
This commit is contained in:
@@ -8,7 +8,10 @@ import type {
|
||||
WorkbenchDocumentRanking,
|
||||
WorkbenchOverviewQuery,
|
||||
WorkbenchOverviewResponse,
|
||||
SigningApiOptions,
|
||||
SigningTemplate,
|
||||
SigningTaskRecord,
|
||||
SigningTaskQuery,
|
||||
WorkbenchTodoTask,
|
||||
WorkbenchTrendPoint,
|
||||
} from './types'
|
||||
@@ -26,6 +29,7 @@ const mockCampusOverviews: Record<WorkbenchCampus, MockCampusOverview> = {
|
||||
本部院区: {
|
||||
summary: {
|
||||
todaySigned: 36,
|
||||
todaySignedChange: 2,
|
||||
pendingPatientSigning: 18,
|
||||
todayOverdue: 2,
|
||||
availableTemplates: 128,
|
||||
@@ -93,6 +97,7 @@ const mockCampusOverviews: Record<WorkbenchCampus, MockCampusOverview> = {
|
||||
东院区: {
|
||||
summary: {
|
||||
todaySigned: 22,
|
||||
todaySignedChange: 1,
|
||||
pendingPatientSigning: 11,
|
||||
todayOverdue: 1,
|
||||
availableTemplates: 76,
|
||||
@@ -160,6 +165,7 @@ const mockCampusOverviews: Record<WorkbenchCampus, MockCampusOverview> = {
|
||||
西院区: {
|
||||
summary: {
|
||||
todaySigned: 17,
|
||||
todaySignedChange: 2,
|
||||
pendingPatientSigning: 9,
|
||||
todayOverdue: 2,
|
||||
availableTemplates: 54,
|
||||
@@ -246,6 +252,47 @@ function createRanking(
|
||||
.sort((left, right) => right.signedCount - left.signedCount)
|
||||
}
|
||||
|
||||
function countAvailableTemplates(templates: SigningTemplate[]) {
|
||||
return new Set(templates.map((template) => template.id)).size
|
||||
}
|
||||
|
||||
function countCoveredDepartments(templates: SigningTemplate[]) {
|
||||
return new Set(
|
||||
templates
|
||||
.map((template) => {
|
||||
if (template.departmentId) {
|
||||
return `id:${template.departmentId}`
|
||||
}
|
||||
|
||||
return template.department && template.department !== '全院通用'
|
||||
? `name:${template.department}`
|
||||
: null
|
||||
})
|
||||
.filter((department): department is string => Boolean(department)),
|
||||
).size
|
||||
}
|
||||
|
||||
async function getAllSigningTasks(
|
||||
query: SigningTaskQuery,
|
||||
options: SigningApiOptions,
|
||||
): Promise<SigningTaskRecord[]> {
|
||||
const pageSize = 200
|
||||
const firstPage = await getSigningTasks({ ...query, page: 1, pageSize }, options)
|
||||
const totalPages = Math.ceil(firstPage.total / Math.max(firstPage.pageSize, 1))
|
||||
|
||||
if (totalPages <= 1) {
|
||||
return firstPage.records
|
||||
}
|
||||
|
||||
const remainingPages = await Promise.all(
|
||||
Array.from({ length: totalPages - 1 }, (_, index) =>
|
||||
getSigningTasks({ ...query, page: index + 2, pageSize }, options),
|
||||
),
|
||||
)
|
||||
|
||||
return [firstPage, ...remainingPages].flatMap((page) => page.records)
|
||||
}
|
||||
|
||||
function isWorkbenchCampus(value: string): value is WorkbenchCampus {
|
||||
return value === '本部院区' || value === '东院区' || value === '西院区'
|
||||
}
|
||||
@@ -268,8 +315,31 @@ function isSameDay(left: Date | null, right: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
function getTaskDate(task: SigningTaskRecord) {
|
||||
return parseDate(task.createdAt) ?? parseDate(task.updatedAt)
|
||||
function getSignedDate(task: SigningTaskRecord) {
|
||||
return parseDate(task.signedAt)
|
||||
}
|
||||
|
||||
function getExpiredDate(task: SigningTaskRecord) {
|
||||
return parseDate(task.expiresAt)
|
||||
}
|
||||
|
||||
function getUpdatedDate(task: SigningTaskRecord) {
|
||||
return parseDate(task.updatedAt) ?? parseDate(task.createdAt)
|
||||
}
|
||||
|
||||
function isWithinPeriod(value: Date | null, today: Date, period: HomeRankingPeriod) {
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const start = new Date(today)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
start.setDate(start.getDate() - period + 1)
|
||||
|
||||
const end = new Date(today)
|
||||
end.setHours(23, 59, 59, 999)
|
||||
|
||||
return value >= start && value <= end
|
||||
}
|
||||
|
||||
function isPendingTask(task: SigningTaskRecord) {
|
||||
@@ -284,10 +354,15 @@ function createLiveOverview(
|
||||
): WorkbenchOverviewResponse {
|
||||
const today = new Date()
|
||||
const signedTasks = tasks.filter((task) => task.status === 'signed')
|
||||
const todaySigned = signedTasks.filter((task) => isSameDay(getTaskDate(task), today)).length
|
||||
const todaySigned = signedTasks.filter((task) => isSameDay(getSignedDate(task), today)).length
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const yesterdaySigned = signedTasks.filter((task) =>
|
||||
isSameDay(getSignedDate(task), yesterday),
|
||||
).length
|
||||
const pendingTasks = tasks.filter(isPendingTask)
|
||||
const todayOverdue = tasks.filter(
|
||||
(task) => task.status === 'expired' && isSameDay(getTaskDate(task), today),
|
||||
(task) => task.status === 'expired' && isSameDay(getExpiredDate(task), today),
|
||||
).length
|
||||
|
||||
const trend = Array.from({ length: rankingPeriod }, (_, index) => {
|
||||
@@ -297,15 +372,15 @@ function createLiveOverview(
|
||||
|
||||
return {
|
||||
label: `${date.getMonth() + 1}/${date.getDate()}`,
|
||||
value: signedTasks.filter((task) => isSameDay(getTaskDate(task), date)).length,
|
||||
value: signedTasks.filter((task) => isSameDay(getSignedDate(task), date)).length,
|
||||
}
|
||||
})
|
||||
|
||||
const todos = tasks
|
||||
.filter((task) => isPendingTask(task) || task.status === 'expired')
|
||||
.sort((left, right) => {
|
||||
const leftDate = getTaskDate(left)?.getTime() ?? 0
|
||||
const rightDate = getTaskDate(right)?.getTime() ?? 0
|
||||
const leftDate = getUpdatedDate(left)?.getTime() ?? 0
|
||||
const rightDate = getUpdatedDate(right)?.getTime() ?? 0
|
||||
return rightDate - leftDate
|
||||
})
|
||||
.slice(0, 8)
|
||||
@@ -321,19 +396,22 @@ function createLiveOverview(
|
||||
}))
|
||||
|
||||
const rankingMap = new Map<string, WorkbenchDocumentRanking>()
|
||||
signedTasks.forEach((task) => {
|
||||
const current = rankingMap.get(task.documentName)
|
||||
rankingMap.set(task.documentName, {
|
||||
id: current?.id ?? task.documentId,
|
||||
documentName: task.documentName,
|
||||
department: task.department,
|
||||
signedCount: (current?.signedCount ?? 0) + 1,
|
||||
signedTasks
|
||||
.filter((task) => isWithinPeriod(getSignedDate(task), today, rankingPeriod))
|
||||
.forEach((task) => {
|
||||
const current = rankingMap.get(task.documentId)
|
||||
rankingMap.set(task.documentId, {
|
||||
id: current?.id ?? task.documentId,
|
||||
documentName: task.documentName,
|
||||
department: task.department,
|
||||
signedCount: (current?.signedCount ?? 0) + 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
summary: {
|
||||
todaySigned,
|
||||
todaySignedChange: todaySigned - yesterdaySigned,
|
||||
pendingPatientSigning: pendingTasks.length,
|
||||
todayOverdue,
|
||||
availableTemplates: templateCount,
|
||||
@@ -361,38 +439,35 @@ export function getWorkbenchOverview(
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.allSettled([getSigningTemplates(), getCampuses()]).then(
|
||||
async ([templatesResult, campusesResult]) => {
|
||||
const templates = templatesResult.status === 'fulfilled' ? templatesResult.value : []
|
||||
const campusNames: Record<string, WorkbenchCampus> =
|
||||
campusesResult.status === 'fulfilled'
|
||||
? (Object.fromEntries(
|
||||
campusesResult.value
|
||||
.filter((campus) => isWorkbenchCampus(campus.name))
|
||||
.map((campus) => [campus.id, campus.name]),
|
||||
) as Record<string, WorkbenchCampus>)
|
||||
: {}
|
||||
const tasksResponse = await getSigningTasks(
|
||||
{
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
status: 'all',
|
||||
dateRange: 'all',
|
||||
campus: 'all',
|
||||
method: 'all',
|
||||
},
|
||||
{ templates, campusNames },
|
||||
)
|
||||
const scopedTasks = Object.keys(campusNames).length
|
||||
? tasksResponse.records.filter((task) => task.campus === query.campus)
|
||||
: tasksResponse.records
|
||||
return getCampuses().then(async (campuses) => {
|
||||
const campus = campuses.find((item) => item.name === query.campus)
|
||||
|
||||
return createLiveOverview(
|
||||
scopedTasks,
|
||||
templates.length,
|
||||
new Set(templates.map((template) => template.department)).size,
|
||||
query.rankingPeriod,
|
||||
)
|
||||
},
|
||||
)
|
||||
if (!campus) {
|
||||
throw new Error(`未找到院区:${query.campus}`)
|
||||
}
|
||||
|
||||
const campusIds = Object.fromEntries(campuses.map((item) => [item.name, item.id]))
|
||||
const campusNames = Object.fromEntries(
|
||||
campuses.filter((item) => isWorkbenchCampus(item.name)).map((item) => [item.id, item.name]),
|
||||
) as Record<string, WorkbenchCampus>
|
||||
const templates = await getSigningTemplates({ campusId: campus.id })
|
||||
const tasks = await getAllSigningTasks(
|
||||
{
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
status: 'all',
|
||||
dateRange: 'all',
|
||||
campus: query.campus,
|
||||
method: 'all',
|
||||
},
|
||||
{ templates, campusIds, campusNames },
|
||||
)
|
||||
|
||||
return createLiveOverview(
|
||||
tasks,
|
||||
countAvailableTemplates(templates),
|
||||
countCoveredDepartments(templates),
|
||||
query.rankingPeriod,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface SigningTemplate {
|
||||
|
||||
export interface WorkbenchSummary {
|
||||
todaySigned: number
|
||||
todaySignedChange: number
|
||||
pendingPatientSigning: number
|
||||
todayOverdue: number
|
||||
availableTemplates: number
|
||||
|
||||
@@ -27,11 +27,21 @@ const loading = ref(true)
|
||||
const loadFailed = ref(false)
|
||||
const overview = ref<WorkbenchOverviewResponse | null>(null)
|
||||
const rankingPeriod = ref<HomeRankingPeriod>(14)
|
||||
let overviewRequestId = 0
|
||||
|
||||
const selectedCampus = computed<WorkbenchCampus>(() => appStore.selectedCampus)
|
||||
|
||||
const metrics = computed<HomeMetricCardData[]>(() => {
|
||||
const summary = overview.value?.summary
|
||||
const signedChange = summary?.todaySignedChange ?? 0
|
||||
const signedChangeTone: HomeMetricCardData['detailTone'] =
|
||||
signedChange > 0 ? 'up' : signedChange < 0 ? 'down' : 'muted'
|
||||
const signedChangeDetail =
|
||||
signedChange > 0
|
||||
? `▲ 较昨日 +${signedChange} · 点击查看`
|
||||
: signedChange < 0
|
||||
? `▼ 较昨日 ${signedChange} · 点击查看`
|
||||
: '较昨日持平 · 点击查看'
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -39,8 +49,8 @@ const metrics = computed<HomeMetricCardData[]>(() => {
|
||||
label: '今日已签署(单)',
|
||||
value: summary?.todaySigned ?? 0,
|
||||
tone: 'success',
|
||||
detail: '▲ 较昨日 +2 · 点击查看',
|
||||
detailTone: 'up',
|
||||
detail: signedChangeDetail,
|
||||
detailTone: signedChangeTone,
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
@@ -74,26 +84,42 @@ const todoItems = computed<HomeTodoItem[]>(() => overview.value?.todos ?? [])
|
||||
const rankingRows = computed<HomeDocumentRanking[]>(() => overview.value?.documentRanking ?? [])
|
||||
|
||||
async function loadOverview() {
|
||||
const requestId = ++overviewRequestId
|
||||
loading.value = true
|
||||
loadFailed.value = false
|
||||
|
||||
try {
|
||||
overview.value = await getWorkbenchOverview({
|
||||
const nextOverview = await getWorkbenchOverview({
|
||||
campus: selectedCampus.value,
|
||||
rankingPeriod: rankingPeriod.value,
|
||||
})
|
||||
if (requestId !== overviewRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
overview.value = nextOverview
|
||||
} catch {
|
||||
if (requestId !== overviewRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
overview.value = null
|
||||
loadFailed.value = true
|
||||
ElMessage.error('首页数据加载失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (requestId === overviewRequestId) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goToSigning(query: Record<string, string> = {}) {
|
||||
void router.push({
|
||||
name: 'workbench-signing',
|
||||
query,
|
||||
query: {
|
||||
campus: selectedCampus.value,
|
||||
...query,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const STATUS_VALUES: SigningTaskStatus[] = [
|
||||
'failed',
|
||||
]
|
||||
const DATE_RANGE_VALUES: SigningDateRange[] = ['today', 'yesterday', '3d', '7d', 'all']
|
||||
const CAMPUS_VALUES: WorkbenchCampus[] = ['本部院区', '东院区', '西院区']
|
||||
|
||||
const statusLabels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
@@ -163,12 +164,18 @@ function isSigningDateRange(value: string): value is SigningDateRange {
|
||||
return DATE_RANGE_VALUES.includes(value as SigningDateRange)
|
||||
}
|
||||
|
||||
function isWorkbenchCampus(value: string): value is WorkbenchCampus {
|
||||
return CAMPUS_VALUES.includes(value as WorkbenchCampus)
|
||||
}
|
||||
|
||||
function syncFilterFromRoute() {
|
||||
const status = getQueryString(route.query.status)
|
||||
const range = getQueryString(route.query.range)
|
||||
const campus = getQueryString(route.query.campus)
|
||||
|
||||
filter.status = status === 'all' || isSigningStatus(status) ? status : 'all'
|
||||
filter.dateRange = isSigningDateRange(range) ? range : 'all'
|
||||
filter.campus = isWorkbenchCampus(campus) ? campus : 'all'
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
@@ -305,10 +312,10 @@ async function loadTasks(preferredTaskId?: string, resetPage = false) {
|
||||
|
||||
const taskIds = new Set(response.records.map((task) => task.id))
|
||||
const routeTaskId = getQueryString(route.query.taskId)
|
||||
const nextTaskId =
|
||||
[preferredTaskId, selectedTaskId.value, routeTaskId].find((id) => id && taskIds.has(id)) ??
|
||||
response.records[0]?.id ??
|
||||
null
|
||||
const matchedTaskId = [preferredTaskId, selectedTaskId.value, routeTaskId].find(
|
||||
(id) => id && taskIds.has(id),
|
||||
)
|
||||
const nextTaskId = matchedTaskId ?? (routeTaskId || response.records[0]?.id || null)
|
||||
|
||||
selectedTaskId.value = nextTaskId
|
||||
await loadTaskDetail(nextTaskId)
|
||||
@@ -561,7 +568,7 @@ async function handleTaskCreated(task: SigningTaskRecord) {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.status, route.query.range, route.query.taskId],
|
||||
() => [route.query.status, route.query.range, route.query.campus, route.query.taskId],
|
||||
() => {
|
||||
syncFilterFromRoute()
|
||||
page.value = 1
|
||||
|
||||
Reference in New Issue
Block a user