449 lines
14 KiB
TypeScript
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))
|
|
}
|