|
|
|
@@ -1,21 +1,23 @@
|
|
|
|
|
import { getCampuses } from '@/api/management/organization'
|
|
|
|
|
|
|
|
|
|
import { getSigningTasks, getSigningTemplates } from './signing'
|
|
|
|
|
import { unwrapApiResponse } from '@/utils/api-response'
|
|
|
|
|
import { request } from '@/utils/request'
|
|
|
|
|
import type { ApiResponse } from '@/types/common'
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
BackendSigningMethod,
|
|
|
|
|
BackendSigningTaskStatus,
|
|
|
|
|
HomeRankingPeriod,
|
|
|
|
|
SigningMethod,
|
|
|
|
|
WorkbenchCampus,
|
|
|
|
|
WorkbenchDocumentRanking,
|
|
|
|
|
WorkbenchOverviewQuery,
|
|
|
|
|
WorkbenchOverviewRequestDto,
|
|
|
|
|
WorkbenchOverviewResponse,
|
|
|
|
|
SigningApiOptions,
|
|
|
|
|
SigningTemplate,
|
|
|
|
|
SigningTaskRecord,
|
|
|
|
|
SigningTaskQuery,
|
|
|
|
|
WorkbenchOverviewResponseDto,
|
|
|
|
|
WorkbenchTodoStatus,
|
|
|
|
|
WorkbenchTodoTask,
|
|
|
|
|
WorkbenchTrendPoint,
|
|
|
|
|
} from './types'
|
|
|
|
|
|
|
|
|
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
|
|
|
|
|
|
|
|
|
interface MockCampusOverview {
|
|
|
|
@@ -252,222 +254,124 @@ function createRanking(
|
|
|
|
|
.sort((left, right) => right.signedCount - left.signedCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countAvailableTemplates(templates: SigningTemplate[]) {
|
|
|
|
|
return new Set(templates.map((template) => template.id)).size
|
|
|
|
|
}
|
|
|
|
|
const TODO_LIMIT = 8
|
|
|
|
|
const RANKING_LIMIT = 10
|
|
|
|
|
let campusIds: Map<string, string> | null = null
|
|
|
|
|
let campusIdsRequest: Promise<Map<string, string>> | null = null
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
function mapTodoStatus(status: BackendSigningTaskStatus): WorkbenchTodoStatus {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'CREATED':
|
|
|
|
|
return 'created'
|
|
|
|
|
case 'WAITING_SIGN':
|
|
|
|
|
return 'pending'
|
|
|
|
|
case 'GENERATING':
|
|
|
|
|
return 'generating'
|
|
|
|
|
case 'EXPIRED':
|
|
|
|
|
return 'overdue'
|
|
|
|
|
case 'SIGNED':
|
|
|
|
|
return 'signed'
|
|
|
|
|
case 'VOIDED':
|
|
|
|
|
return 'voided'
|
|
|
|
|
case 'FAILED':
|
|
|
|
|
default:
|
|
|
|
|
return 'failed'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 === '西院区'
|
|
|
|
|
function mapSigningMethod(method: BackendSigningMethod): SigningMethod {
|
|
|
|
|
return method === 'SMS' ? 'sms' : 'pad'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseDate(value: string | undefined) {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const date = new Date(value.includes('T') ? value : value.replace(' ', 'T'))
|
|
|
|
|
return Number.isNaN(date.getTime()) ? null : date
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSameDay(left: Date | null, right: Date) {
|
|
|
|
|
return Boolean(
|
|
|
|
|
left &&
|
|
|
|
|
left.getFullYear() === right.getFullYear() &&
|
|
|
|
|
left.getMonth() === right.getMonth() &&
|
|
|
|
|
left.getDate() === right.getDate(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
return task.status === 'pending' || task.status === 'signing'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createLiveOverview(
|
|
|
|
|
tasks: SigningTaskRecord[],
|
|
|
|
|
templateCount: number,
|
|
|
|
|
coveredDepartments: number,
|
|
|
|
|
rankingPeriod: HomeRankingPeriod,
|
|
|
|
|
): WorkbenchOverviewResponse {
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const signedTasks = tasks.filter((task) => task.status === 'signed')
|
|
|
|
|
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(getExpiredDate(task), today),
|
|
|
|
|
).length
|
|
|
|
|
|
|
|
|
|
const trend = Array.from({ length: rankingPeriod }, (_, index) => {
|
|
|
|
|
const date = new Date(today)
|
|
|
|
|
date.setHours(0, 0, 0, 0)
|
|
|
|
|
date.setDate(today.getDate() - rankingPeriod + index + 1)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
label: `${date.getMonth() + 1}/${date.getDate()}`,
|
|
|
|
|
value: signedTasks.filter((task) => isSameDay(getSignedDate(task), date)).length,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const todos = tasks
|
|
|
|
|
.filter((task) => isPendingTask(task) || task.status === 'expired')
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
const leftDate = getUpdatedDate(left)?.getTime() ?? 0
|
|
|
|
|
const rightDate = getUpdatedDate(right)?.getTime() ?? 0
|
|
|
|
|
return rightDate - leftDate
|
|
|
|
|
})
|
|
|
|
|
.slice(0, 8)
|
|
|
|
|
.map((task) => ({
|
|
|
|
|
id: task.id,
|
|
|
|
|
patientId: task.patientId,
|
|
|
|
|
patientName: task.patientName,
|
|
|
|
|
documentName: task.documentName,
|
|
|
|
|
department: task.department,
|
|
|
|
|
status: task.status === 'expired' ? ('overdue' as const) : ('pending' as const),
|
|
|
|
|
method: task.method,
|
|
|
|
|
updatedAt: task.updatedAt,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const rankingMap = new Map<string, WorkbenchDocumentRanking>()
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
function mapWorkbenchOverview(dto: WorkbenchOverviewResponseDto): WorkbenchOverviewResponse {
|
|
|
|
|
const summary = dto.summary
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
summary: {
|
|
|
|
|
todaySigned,
|
|
|
|
|
todaySignedChange: todaySigned - yesterdaySigned,
|
|
|
|
|
pendingPatientSigning: pendingTasks.length,
|
|
|
|
|
todayOverdue,
|
|
|
|
|
availableTemplates: templateCount,
|
|
|
|
|
coveredDepartments,
|
|
|
|
|
todaySigned: summary?.todaySigned ?? 0,
|
|
|
|
|
todaySignedChange: summary?.todaySignedChange ?? 0,
|
|
|
|
|
pendingPatientSigning: summary?.pendingPatientSigning ?? 0,
|
|
|
|
|
todayOverdue: summary?.todayOverdue ?? 0,
|
|
|
|
|
availableTemplates: summary?.availableTemplates ?? 0,
|
|
|
|
|
coveredDepartments: summary?.coveredDepartments ?? 0,
|
|
|
|
|
},
|
|
|
|
|
trend,
|
|
|
|
|
todos,
|
|
|
|
|
documentRanking: [...rankingMap.values()]
|
|
|
|
|
.sort((left, right) => right.signedCount - left.signedCount)
|
|
|
|
|
.slice(0, 10),
|
|
|
|
|
trend: (dto.trend ?? []).map<WorkbenchTrendPoint>((point) => ({
|
|
|
|
|
label: point.label || point.date,
|
|
|
|
|
value: point.signedCount,
|
|
|
|
|
})),
|
|
|
|
|
todos: (dto.todos ?? []).map<WorkbenchTodoTask>((task) => ({
|
|
|
|
|
id: task.taskId,
|
|
|
|
|
patientId: task.patientId,
|
|
|
|
|
patientName: task.patientName,
|
|
|
|
|
documentName: task.documentName,
|
|
|
|
|
department: task.departmentName,
|
|
|
|
|
status: mapTodoStatus(task.status),
|
|
|
|
|
method: mapSigningMethod(task.signMethod),
|
|
|
|
|
updatedAt: task.updatedAt,
|
|
|
|
|
})),
|
|
|
|
|
documentRanking: (dto.documentRanking ?? []).map<WorkbenchDocumentRanking>((item) => ({
|
|
|
|
|
id: item.templateId,
|
|
|
|
|
documentName: item.documentName,
|
|
|
|
|
department: item.departmentName,
|
|
|
|
|
signedCount: item.signedCount,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getWorkbenchOverview(
|
|
|
|
|
function getCampusIds() {
|
|
|
|
|
if (campusIds) {
|
|
|
|
|
return Promise.resolve(campusIds)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!campusIdsRequest) {
|
|
|
|
|
campusIdsRequest = getCampuses()
|
|
|
|
|
.then((campuses) => {
|
|
|
|
|
campusIds = new Map(campuses.map((campus) => [campus.name, campus.id]))
|
|
|
|
|
return campusIds
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
campusIdsRequest = null
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return campusIdsRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveCampusId(campusName: WorkbenchCampus) {
|
|
|
|
|
const campusId = (await getCampusIds()).get(campusName)
|
|
|
|
|
|
|
|
|
|
if (!campusId) {
|
|
|
|
|
throw new Error('未找到院区:' + campusName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return campusId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getWorkbenchOverview(
|
|
|
|
|
query: WorkbenchOverviewQuery,
|
|
|
|
|
): Promise<WorkbenchOverviewResponse> {
|
|
|
|
|
if (useMockData) {
|
|
|
|
|
const overview = mockCampusOverviews[query.campus] ?? mockCampusOverviews['本部院区']
|
|
|
|
|
|
|
|
|
|
return Promise.resolve({
|
|
|
|
|
return {
|
|
|
|
|
summary: { ...overview.summary },
|
|
|
|
|
trend: overview.trend.map((point) => ({ ...point })),
|
|
|
|
|
todos: overview.todos.map((task) => ({ ...task })),
|
|
|
|
|
documentRanking: createRanking(overview.ranking, query.rankingPeriod),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return getCampuses().then(async (campuses) => {
|
|
|
|
|
const campus = campuses.find((item) => item.name === query.campus)
|
|
|
|
|
const campusId = query.campusId || (await resolveCampusId(query.campus))
|
|
|
|
|
const params: WorkbenchOverviewRequestDto = {
|
|
|
|
|
campusId,
|
|
|
|
|
period: query.rankingPeriod,
|
|
|
|
|
todoLimit: TODO_LIMIT,
|
|
|
|
|
rankingLimit: RANKING_LIMIT,
|
|
|
|
|
}
|
|
|
|
|
const response = await request.get<ApiResponse<WorkbenchOverviewResponseDto>>(
|
|
|
|
|
'/v1/workbench/overview',
|
|
|
|
|
{ params },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
return mapWorkbenchOverview(unwrapApiResponse(response))
|
|
|
|
|
}
|
|
|
|
|