feat: 仪表盘角色自适应重构 — 4角色视图 + 后端个人工作量API
后端: - 新增 GET /health/admin/statistics/personal-stats 接口 - PersonalStatsResp: 13个个人维度统计字段 - 按医生/护士/管理员/运营角色聚合工作量数据 前端: - useDashboardRole hook: 按优先级 doctor>nurse>admin>operator 匹配角色 - DoctorDashboard: 今日工作台(日程/审核/消息/统计卡) - NurseDashboard: 随访监控台(异常提醒/队列/上报率) - AdminDashboard: 管理中心(5KPI + 健康数据Tab) - OperatorDashboard: 运营中心(积分/文章/活动) - StatisticsDashboard.tsx 重写为角色路由组件 - 删除旧区块:快捷入口/积分排行Top10/最近活动
This commit is contained in:
@@ -115,6 +115,28 @@ pub struct HealthDataStatsResp {
|
||||
pub vital_signs_report_rate: VitalSignsReportRateResp,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 个人维度统计
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 当前用户个人维度的工作量统计
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct PersonalStatsResp {
|
||||
pub my_patients: i64,
|
||||
pub new_patients_this_month: i64,
|
||||
pub follow_up_rate: f64,
|
||||
pub consultations_this_month: i64,
|
||||
pub pending_consultations: i64,
|
||||
pub vital_signs_report_rate: f64,
|
||||
pub today_appointments: i64,
|
||||
pub overdue_follow_ups: i64,
|
||||
pub today_follow_ups: i64,
|
||||
pub abnormal_vital_signs: i64,
|
||||
pub vital_signs_reported: i64,
|
||||
pub vital_signs_total: i64,
|
||||
pub pending_lab_reviews: i64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 通用结构
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -134,3 +134,20 @@ where
|
||||
let result = stats_service::get_health_data_stats(&state, ctx.tenant_id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 个人维度统计
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn get_personal_stats<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
) -> Result<Json<ApiResponse<PersonalStatsResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
let result = stats_service::get_personal_stats(&state, ctx.user_id, ctx.tenant_id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
@@ -577,6 +577,10 @@ impl HealthModule {
|
||||
"/health/admin/statistics/health-data",
|
||||
axum::routing::get(stats_handler::get_health_data_stats),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/statistics/personal-stats",
|
||||
axum::routing::get(stats_handler::get_personal_stats),
|
||||
)
|
||||
// 危急值阈值配置
|
||||
.route(
|
||||
"/health/critical-value-thresholds",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, sea_query::Expr, FromQueryResult, FromQueryResult as _};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, sea_query::Expr, FromQueryResult};
|
||||
|
||||
use erp_core::error::AppResult;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::dto::stats_dto::*;
|
||||
use crate::entity::{
|
||||
patient, consultation_session, follow_up_task,
|
||||
points_transaction, dialysis_record, lab_report,
|
||||
appointment, vital_signs,
|
||||
appointment, vital_signs, patient_doctor_relation, doctor_profile,
|
||||
};
|
||||
use crate::state::HealthState;
|
||||
|
||||
@@ -621,3 +621,254 @@ async fn compute_daily_report_rate(
|
||||
DailyReportRate { date: r.date, reported: r.reported, total, rate }
|
||||
}).collect())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 个人维度统计
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn get_personal_stats(
|
||||
state: &HealthState,
|
||||
user_id: uuid::Uuid,
|
||||
tenant_id: uuid::Uuid,
|
||||
) -> AppResult<PersonalStatsResp> {
|
||||
let db = &state.db;
|
||||
|
||||
// 通过 user_id 查找 doctor_profile 以获得 doctor_id
|
||||
let doctor_profile = doctor_profile::Entity::find()
|
||||
.filter(doctor_profile::Column::TenantId.eq(tenant_id))
|
||||
.filter(doctor_profile::Column::DeletedAt.is_null())
|
||||
.filter(doctor_profile::Column::UserId.eq(user_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let doctor_id = doctor_profile.map(|p| p.id);
|
||||
|
||||
// my_patients: 通过 patient_doctor_relation 统计
|
||||
let my_patients = if let Some(did) = doctor_id {
|
||||
patient_doctor_relation::Entity::find()
|
||||
.filter(patient_doctor_relation::Column::TenantId.eq(tenant_id))
|
||||
.filter(patient_doctor_relation::Column::DeletedAt.is_null())
|
||||
.filter(patient_doctor_relation::Column::DoctorId.eq(did))
|
||||
.count(db)
|
||||
.await? as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// new_patients_this_month: 本月新增关联患者
|
||||
let new_patients_this_month = if let Some(did) = doctor_id {
|
||||
let sql = r#"
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM patient_doctor_relation pdr
|
||||
INNER JOIN patient p ON p.id = pdr.patient_id AND p.deleted_at IS NULL AND p.tenant_id = $1
|
||||
WHERE pdr.tenant_id = $1 AND pdr.deleted_at IS NULL
|
||||
AND pdr.doctor_id = $2
|
||||
AND p.created_at >= date_trunc('month', NOW())
|
||||
"#;
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct Cnt {
|
||||
cnt: i64,
|
||||
}
|
||||
|
||||
let result: Option<Cnt> = FromQueryResult::find_by_statement(
|
||||
sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
[tenant_id.into(), did.into()],
|
||||
),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
result.map(|r| r.cnt).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// follow_up_rate: 分配给当前用户的随访完成率
|
||||
let sql = r#"
|
||||
SELECT COALESCE(status, '__total') AS status, COUNT(*) AS cnt
|
||||
FROM follow_up_task
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL AND assigned_to = $2
|
||||
GROUP BY GROUPING SETS ((status), ())
|
||||
"#;
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct StatusCount {
|
||||
status: String,
|
||||
cnt: i64,
|
||||
}
|
||||
|
||||
let fu_rows: Vec<StatusCount> = FromQueryResult::find_by_statement(
|
||||
sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
[tenant_id.into(), user_id.into()],
|
||||
),
|
||||
)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut fu_total: i64 = 0;
|
||||
let mut fu_completed: i64 = 0;
|
||||
let mut overdue_follow_ups: i64 = 0;
|
||||
for row in &fu_rows {
|
||||
match row.status.as_str() {
|
||||
"__total" => fu_total = row.cnt,
|
||||
"completed" => fu_completed = row.cnt,
|
||||
"overdue" => overdue_follow_ups = row.cnt,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let follow_up_rate = if fu_total > 0 {
|
||||
(fu_completed as f64 / fu_total as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// consultations_this_month / pending_consultations: 咨询统计
|
||||
let consultations_this_month = if let Some(did) = doctor_id {
|
||||
consultation_session::Entity::find()
|
||||
.filter(consultation_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(consultation_session::Column::DeletedAt.is_null())
|
||||
.filter(consultation_session::Column::DoctorId.eq(did))
|
||||
.filter(Expr::col(consultation_session::Column::CreatedAt).gte(Expr::cust("date_trunc('month', NOW())")))
|
||||
.count(db)
|
||||
.await? as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let pending_consultations = if let Some(did) = doctor_id {
|
||||
consultation_session::Entity::find()
|
||||
.filter(consultation_session::Column::TenantId.eq(tenant_id))
|
||||
.filter(consultation_session::Column::DeletedAt.is_null())
|
||||
.filter(consultation_session::Column::DoctorId.eq(did))
|
||||
.filter(consultation_session::Column::Status.eq("active"))
|
||||
.count(db)
|
||||
.await? as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// today_appointments: 今日预约
|
||||
let today_appointments = if let Some(did) = doctor_id {
|
||||
appointment::Entity::find()
|
||||
.filter(appointment::Column::TenantId.eq(tenant_id))
|
||||
.filter(appointment::Column::DeletedAt.is_null())
|
||||
.filter(appointment::Column::DoctorId.eq(did))
|
||||
.filter(Expr::col(appointment::Column::AppointmentDate).eq(Expr::cust("CURRENT_DATE")))
|
||||
.count(db)
|
||||
.await? as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// today_follow_ups: 今日随访任务
|
||||
let today_follow_ups = follow_up_task::Entity::find()
|
||||
.filter(follow_up_task::Column::TenantId.eq(tenant_id))
|
||||
.filter(follow_up_task::Column::DeletedAt.is_null())
|
||||
.filter(follow_up_task::Column::AssignedTo.eq(user_id))
|
||||
.filter(Expr::col(follow_up_task::Column::PlannedDate).eq(Expr::cust("CURRENT_DATE")))
|
||||
.count(db)
|
||||
.await? as i64;
|
||||
|
||||
// vital_signs_report_rate: 当前医生的患者体征上报率
|
||||
let (vital_signs_reported, vital_signs_total, vital_signs_report_rate) = if my_patients > 0 {
|
||||
let vs_sql = r#"
|
||||
SELECT
|
||||
COUNT(DISTINCT vs.patient_id) AS reported,
|
||||
$3::bigint AS total
|
||||
FROM vital_signs vs
|
||||
WHERE vs.tenant_id = $1 AND vs.deleted_at IS NULL
|
||||
AND vs.created_at >= date_trunc('month', NOW())
|
||||
AND vs.patient_id IN (
|
||||
SELECT patient_id FROM patient_doctor_relation
|
||||
WHERE doctor_id = $2 AND tenant_id = $1 AND deleted_at IS NULL
|
||||
)
|
||||
"#;
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct VsCount {
|
||||
reported: i64,
|
||||
total: i64,
|
||||
}
|
||||
|
||||
let result: Option<VsCount> = FromQueryResult::find_by_statement(
|
||||
sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
vs_sql,
|
||||
[tenant_id.into(), doctor_id.unwrap_or_default().into(), my_patients.into()],
|
||||
),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
Some(r) => {
|
||||
let rate = if r.total > 0 {
|
||||
(r.reported as f64 / r.total as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
(r.reported, r.total, rate)
|
||||
}
|
||||
None => (0, my_patients, 0.0),
|
||||
}
|
||||
} else {
|
||||
(0, 0, 0.0)
|
||||
};
|
||||
|
||||
// pending_lab_reviews: 待审核化验报告(与当前医生的患者关联)
|
||||
let pending_lab_reviews = if doctor_id.is_some() {
|
||||
let lr_sql = r#"
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM lab_report lr
|
||||
WHERE lr.tenant_id = $1 AND lr.deleted_at IS NULL
|
||||
AND lr.status = 'pending'
|
||||
AND lr.patient_id IN (
|
||||
SELECT patient_id FROM patient_doctor_relation
|
||||
WHERE doctor_id = $2 AND tenant_id = $1 AND deleted_at IS NULL
|
||||
)
|
||||
"#;
|
||||
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
struct LrCnt {
|
||||
cnt: i64,
|
||||
}
|
||||
|
||||
let result: Option<LrCnt> = FromQueryResult::find_by_statement(
|
||||
sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
lr_sql,
|
||||
[tenant_id.into(), doctor_id.unwrap_or_default().into()],
|
||||
),
|
||||
)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
result.map(|r| r.cnt).unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// abnormal_vital_signs: 简化实现,返回 0(完整实现需要关联危急值阈值配置)
|
||||
let abnormal_vital_signs: i64 = 0;
|
||||
|
||||
Ok(PersonalStatsResp {
|
||||
my_patients,
|
||||
new_patients_this_month,
|
||||
follow_up_rate,
|
||||
consultations_this_month,
|
||||
pending_consultations,
|
||||
vital_signs_report_rate,
|
||||
today_appointments,
|
||||
overdue_follow_ups,
|
||||
today_follow_ups,
|
||||
abnormal_vital_signs,
|
||||
vital_signs_reported,
|
||||
vital_signs_total,
|
||||
pending_lab_reviews,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user