Files
xh-medical-sign-web/clinical-web/src/api/workbench/home.ts
T

449 lines
14 KiB
TypeScript

import { getCampuses } from '@/api/management/organization'
import { unwrapApiResponse } from '@/utils/api-response'
import { mockFlags } from '@/utils/mock-flags'
import { request } from '@/utils/request'
import type { ApiResponse } from '@/types/common'
import type {
BackendSigningMethod,
BackendSigningTaskStatus,
HomeRankingPeriod,
SigningMethod,
WorkbenchCampus,
WorkbenchDocumentRanking,
WorkbenchOverviewQuery,
WorkbenchOverviewRequestDto,
WorkbenchOverviewResponse,
WorkbenchOverviewResponseDto,
WorkbenchTodoStatus,
WorkbenchTodoTask,
WorkbenchTrendPoint,
} from './types'
const useMockData = mockFlags.workbenchHome
interface MockCampusOverview {
summary: WorkbenchOverviewResponse['summary']
trend: WorkbenchTrendPoint[]
todos: WorkbenchTodoTask[]
ranking: Array<Omit<WorkbenchDocumentRanking, 'signedCount'> & { baseCount: number }>
}
const mockCampusOverviews: Record<WorkbenchCampus, MockCampusOverview> = {
本部院区: {
summary: {
todaySigned: 36,
todaySignedChange: 2,
pendingPatientSigning: 18,
todayOverdue: 2,
availableTemplates: 128,
coveredDepartments: 9,
},
trend: [
{ label: '8/21', value: 24 },
{ label: '8/22', value: 31 },
{ label: '8/23', value: 28 },
{ label: '8/24', value: 35 },
{ label: '8/25', value: 32 },
{ label: '8/26', value: 39 },
{ label: '8/27', value: 36 },
],
todos: [
{
id: 'T0818003',
patientId: 'P202610005',
patientName: '陈志强',
documentName: '住院须知及告知书',
department: '消化内科',
status: 'pending',
method: 'pad',
updatedAt: '2026-08-27 11:20',
},
{
id: 'T0818002',
patientId: 'P202610002',
patientName: '李淑芬',
documentName: 'MRI 磁共振检查知情同意书',
department: '放射科',
status: 'pending',
method: 'sms',
updatedAt: '2026-08-27 10:05',
},
{
id: 'T0815001',
patientId: 'P202610001',
patientName: '王建国',
documentName: '心包穿刺知情同意书',
department: '心内科',
status: 'overdue',
method: 'sms',
updatedAt: '2026-08-27 09:40',
},
{
// Pad 移动端:待办里要四种方式都有样本,徽标配色才看得出区别
id: 'T0818004',
patientId: 'P202610003',
patientName: '赵某某',
documentName: '住院须知及告知书',
department: '消化内科',
status: 'pending',
method: 'ipad',
updatedAt: '2026-08-27 09:05',
},
{
id: 'T0818005',
patientId: 'P202610007',
patientName: '孙浩然',
documentName: '内镜检查知情同意书',
department: '普外科',
status: 'pending',
method: 'wx',
updatedAt: '2026-08-27 08:50',
},
],
ranking: [
{ id: 'doc-001', documentName: '住院患者知情同意书', department: '全院通用', baseCount: 58 },
{ id: 'doc-002', documentName: '入院须知及告知书', department: '全院通用', baseCount: 51 },
{
id: 'doc-003',
documentName: 'MRI 磁共振检查知情同意书',
department: '放射科',
baseCount: 43,
},
{ id: 'doc-004', documentName: '手术知情同意书', department: '普外科', baseCount: 39 },
{ id: 'doc-005', documentName: '麻醉知情同意书', department: '麻醉科', baseCount: 35 },
{ id: 'doc-006', documentName: '患者授权委托书', department: '医务处', baseCount: 31 },
{ id: 'doc-007', documentName: '输血治疗知情同意书', department: '血液科', baseCount: 27 },
{ id: 'doc-008', documentName: '内镜检查知情同意书', department: '消化内科', baseCount: 24 },
{ id: 'doc-009', documentName: '介入治疗知情同意书', department: '心内科', baseCount: 21 },
{ id: 'doc-010', documentName: '特殊检查风险告知书', department: '影像科', baseCount: 18 },
],
},
东院区: {
summary: {
todaySigned: 22,
todaySignedChange: 1,
pendingPatientSigning: 11,
todayOverdue: 1,
availableTemplates: 76,
coveredDepartments: 6,
},
trend: [
{ label: '8/21', value: 15 },
{ label: '8/22', value: 18 },
{ label: '8/23', value: 17 },
{ label: '8/24', value: 23 },
{ label: '8/25', value: 20 },
{ label: '8/26', value: 25 },
{ label: '8/27', value: 22 },
],
todos: [
{
id: 'T0818001',
patientId: 'P202610004',
patientName: '赵美英',
documentName: '体检报告发放及知情同意书',
department: '健康管理中心',
status: 'pending',
method: 'pad',
updatedAt: '2026-08-27 09:12',
},
{
id: 'T0818011',
patientId: 'P202610011',
patientName: '周雅琴',
documentName: '检查前注意事项告知书',
department: '体检中心',
status: 'overdue',
method: 'sms',
updatedAt: '2026-08-27 08:30',
},
{
id: 'T0818012',
patientId: 'P202610003',
patientName: '赵某某',
documentName: '住院须知及告知书',
department: '消化内科',
status: 'pending',
method: 'ipad',
updatedAt: '2026-08-27 09:05',
},
{
id: 'T0818013',
patientId: 'P202610007',
patientName: '孙浩然',
documentName: '内镜检查知情同意书',
department: '普外科',
status: 'pending',
method: 'wx',
updatedAt: '2026-08-27 08:50',
},
],
ranking: [
{
id: 'doc-011',
documentName: '体检报告发放及知情同意书',
department: '健康管理中心',
baseCount: 46,
},
{ id: 'doc-012', documentName: '检前告知书', department: '健康管理中心', baseCount: 39 },
{
id: 'doc-013',
documentName: '超声检查知情同意书',
department: '超声医学科',
baseCount: 32,
},
{
id: 'doc-014',
documentName: '胃肠镜检查知情同意书',
department: '消化内科',
baseCount: 28,
},
{ id: 'doc-015', documentName: '采血及检验告知书', department: '检验科', baseCount: 25 },
{ id: 'doc-016', documentName: '影像检查告知书', department: '影像科', baseCount: 21 },
{ id: 'doc-017', documentName: '健康体检知情同意书', department: '体检中心', baseCount: 19 },
{ id: 'doc-018', documentName: '隐私保护告知书', department: '健康管理中心', baseCount: 16 },
{ id: 'doc-019', documentName: '麻醉检查知情同意书', department: '麻醉科', baseCount: 14 },
{ id: 'doc-020', documentName: '报告领取授权书', department: '健康管理中心', baseCount: 12 },
],
},
西院区: {
summary: {
todaySigned: 17,
todaySignedChange: 2,
pendingPatientSigning: 9,
todayOverdue: 2,
availableTemplates: 54,
coveredDepartments: 5,
},
trend: [
{ label: '8/21', value: 11 },
{ label: '8/22', value: 14 },
{ label: '8/23', value: 13 },
{ label: '8/24', value: 16 },
{ label: '8/25', value: 15 },
{ label: '8/26', value: 19 },
{ label: '8/27', value: 17 },
],
todos: [
{
id: 'T0818021',
patientId: 'P202610021',
patientName: '孙浩然',
documentName: '心脏介入手术知情同意书',
department: '心内科',
status: 'pending',
method: 'pad',
updatedAt: '2026-08-27 10:15',
},
{
id: 'T0818022',
patientId: 'P202610022',
patientName: '刘晓琴',
documentName: '骨科手术知情同意书',
department: '骨科',
status: 'pending',
method: 'sms',
updatedAt: '2026-08-27 09:52',
},
{
id: 'T0818023',
patientId: 'P202610023',
patientName: '何文博',
documentName: '术前麻醉访视告知书',
department: '麻醉科',
status: 'overdue',
method: 'pad',
updatedAt: '2026-08-27 08:05',
},
{
id: 'T0818024',
patientId: 'P202610003',
patientName: '赵某某',
documentName: '住院须知及告知书',
department: '消化内科',
status: 'pending',
method: 'ipad',
updatedAt: '2026-08-27 09:05',
},
{
id: 'T0818025',
patientId: 'P202610007',
patientName: '孙浩然',
documentName: '内镜检查知情同意书',
department: '普外科',
status: 'pending',
method: 'wx',
updatedAt: '2026-08-27 08:50',
},
],
ranking: [
{
id: 'doc-021',
documentName: '心脏介入手术知情同意书',
department: '心内科',
baseCount: 37,
},
{ id: 'doc-022', documentName: '骨科手术知情同意书', department: '骨科', baseCount: 33 },
{ id: 'doc-023', documentName: '术前麻醉访视告知书', department: '麻醉科', baseCount: 28 },
{ id: 'doc-024', documentName: '住院患者知情同意书', department: '全院通用', baseCount: 25 },
{ id: 'doc-025', documentName: '输血治疗知情同意书', department: '血液科', baseCount: 22 },
{ id: 'doc-026', documentName: '介入治疗知情同意书', department: '心内科', baseCount: 20 },
{ id: 'doc-027', documentName: '术后康复告知书', department: '骨科', baseCount: 18 },
{ id: 'doc-028', documentName: '特殊检查风险告知书', department: '影像科', baseCount: 16 },
{ id: 'doc-029', documentName: '患者授权委托书', department: '医务处', baseCount: 14 },
{ id: 'doc-030', documentName: '手术部位标识确认书', department: '手术部', baseCount: 12 },
],
},
}
function getPeriodFactor(period: HomeRankingPeriod) {
return period === 7 ? 1 : period === 14 ? 2 : 3.5
}
function createRanking(
rows: MockCampusOverview['ranking'],
period: HomeRankingPeriod,
): WorkbenchDocumentRanking[] {
const factor = getPeriodFactor(period)
return rows
.map((row, index) => ({
id: row.id,
documentName: row.documentName,
department: row.department,
signedCount: Math.round(row.baseCount * factor + (index % 3)),
}))
.sort((left, right) => right.signedCount - left.signedCount)
}
const TODO_LIMIT = 8
const RANKING_LIMIT = 10
let campusIds: Map<string, string> | null = null
let campusIdsRequest: Promise<Map<string, string>> | null = null
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 METHOD_BY_BACKEND: Record<BackendSigningMethod, SigningMethod> = {
PAD: 'pad',
SMS: 'sms',
IPAD: 'ipad',
WX: 'wx',
}
function mapSigningMethod(method: BackendSigningMethod): SigningMethod {
// 后端理论上只回这四个;未知取值按手写板兜底,避免整行渲染不出来
return METHOD_BY_BACKEND[method] ?? 'pad'
}
function mapWorkbenchOverview(dto: WorkbenchOverviewResponseDto): WorkbenchOverviewResponse {
const summary = dto.summary
return {
summary: {
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: (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,
})),
}
}
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 {
summary: { ...overview.summary },
trend: overview.trend.map((point) => ({ ...point })),
todos: overview.todos.map((task) => ({ ...task })),
documentRanking: createRanking(overview.ranking, query.rankingPeriod),
}
}
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 },
)
return mapWorkbenchOverview(unwrapApiResponse(response))
}