diff --git a/crates/erp-health/src/handler/action_inbox_handler.rs b/crates/erp-health/src/handler/action_inbox_handler.rs index 01c2334..58e41cd 100644 --- a/crates/erp-health/src/handler/action_inbox_handler.rs +++ b/crates/erp-health/src/handler/action_inbox_handler.rs @@ -5,7 +5,8 @@ use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext}; use crate::service::action_inbox_service::{ - self, ActionInboxQuery, ActionItem, TeamOverview, ThreadResponse, WorkbenchStats, + self, ActionInboxQuery, ActionItem, NursePatientSummary, TeamOverview, ThreadResponse, + WorkbenchStats, }; use crate::state::HealthState; @@ -19,8 +20,13 @@ where S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.action-inbox.list")?; + let user_id = if query.assigned_to_me.unwrap_or(false) { + Some(ctx.user_id) + } else { + None + }; let result = - action_inbox_service::list_action_items(&state.db, ctx.tenant_id, &query).await?; + action_inbox_service::list_action_items(&state.db, ctx.tenant_id, user_id, &query).await?; Ok(Json(ApiResponse::ok(result))) } @@ -45,17 +51,28 @@ where pub async fn get_workbench_stats( State(state): State, Extension(ctx): Extension, + Query(params): Query, ) -> Result>, AppError> where HealthState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "health.action-inbox.list")?; + let user_id = if params.assigned_to_me.unwrap_or(false) { + Some(ctx.user_id) + } else { + None + }; let result = - action_inbox_service::get_workbench_stats(&state.db, ctx.tenant_id).await?; + action_inbox_service::get_workbench_stats(&state.db, ctx.tenant_id, user_id).await?; Ok(Json(ApiResponse::ok(result))) } +#[derive(Debug, serde::Deserialize)] +pub struct WorkbenchStatsQuery { + pub assigned_to_me: Option, +} + pub async fn get_team_overview( State(state): State, Extension(ctx): Extension, @@ -69,3 +86,18 @@ where action_inbox_service::get_team_overview(&state.db, ctx.tenant_id).await?; Ok(Json(ApiResponse::ok(result))) } + +/// 获取护士班次患者列表 — 今日分配给当前护士的患者 +pub async fn get_nurse_patients( + State(state): State, + Extension(ctx): Extension, +) -> Result>>, AppError> +where + HealthState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "health.action-inbox.list")?; + let result = + action_inbox_service::get_nurse_patients(&state.db, ctx.tenant_id, ctx.user_id).await?; + Ok(Json(ApiResponse::ok(result))) +} diff --git a/crates/erp-health/src/module.rs b/crates/erp-health/src/module.rs index e812fb3..cd9651d 100644 --- a/crates/erp-health/src/module.rs +++ b/crates/erp-health/src/module.rs @@ -801,6 +801,10 @@ impl HealthModule { "/health/action-inbox/{source_ref}/thread", axum::routing::get(action_inbox_handler::get_action_thread), ) + .route( + "/health/action-inbox/my-patients", + axum::routing::get(action_inbox_handler::get_nurse_patients), + ) // OAuth 合作方管理 .route( "/health/oauth/clients", diff --git a/crates/erp-health/src/service/action_inbox_service.rs b/crates/erp-health/src/service/action_inbox_service.rs index f03e0a3..5bc66af 100644 --- a/crates/erp-health/src/service/action_inbox_service.rs +++ b/crates/erp-health/src/service/action_inbox_service.rs @@ -89,6 +89,17 @@ pub struct ActionInboxQuery { pub action_type: Option, pub page: Option, pub page_size: Option, + pub assigned_to_me: Option, + pub patient_id: Option, +} + +/// 护士班次患者摘要 +#[derive(Debug, Clone, Serialize)] +pub struct NursePatientSummary { + pub patient_id: Uuid, + pub patient_name: String, + pub pending_actions: i64, + pub highest_priority: ActionPriority, } // ── 内部查询结构体 ────────────────────────────────────────────────── @@ -214,13 +225,16 @@ fn summarize_result(result: &Option) -> String { pub async fn list_action_items( db: &DatabaseConnection, tenant_id: Uuid, + user_id: Option, query: &ActionInboxQuery, ) -> Result, HealthError> { - tracing::info!(tenant_id = %tenant_id, status = ?query.status, action_type = ?query.action_type, "列出待办事项"); + tracing::info!(tenant_id = %tenant_id, status = ?query.status, action_type = ?query.action_type, assigned_to_me = ?query.assigned_to_me, "列出待办事项"); let page = query.page.unwrap_or(1).max(1); let page_size = query.page_size.unwrap_or(20).min(100); let offset = (page - 1) * page_size; + let filter_by_user = query.assigned_to_me.unwrap_or(false) && user_id.is_some(); + // 各段的 status 过滤条件 let (sug_status, alert_status, fu_status) = match query.status.as_deref() { Some("pending") => ( @@ -254,6 +268,12 @@ pub async fn list_action_items( let mut segments: Vec = Vec::new(); + // patient_id 过滤条件(所有段共用) + let patient_filter = match &query.patient_id { + Some(pid) => format!("AND patient_id = '{}'", pid), + None => String::new(), + }; + if include_sug { segments.push(format!( r#" @@ -264,7 +284,7 @@ pub async fn list_action_items( FROM ai_suggestion s JOIN ai_analysis a ON s.analysis_id = a.id JOIN patient p ON a.patient_id = p.id - WHERE s.tenant_id = $1 AND s.deleted_at IS NULL {sug_status}"# + WHERE s.tenant_id = $1 AND s.deleted_at IS NULL {sug_status} {patient_filter}"# )); } @@ -277,11 +297,16 @@ pub async fn list_action_items( NULL::text AS result_content, NULL::uuid AS source_id FROM alerts al JOIN patient p ON al.patient_id = p.id - WHERE al.tenant_id = $1 AND al.deleted_at IS NULL {alert_status}"# + WHERE al.tenant_id = $1 AND al.deleted_at IS NULL {alert_status} {patient_filter}"# )); } if include_fu { + let assigned_filter = if filter_by_user { + format!("AND f.assigned_to = '{}'", user_id.unwrap()) + } else { + String::new() + }; segments.push(format!( r#" SELECT f.id, 'followup' AS action_type, 'medium' AS priority_raw, @@ -290,7 +315,7 @@ pub async fn list_action_items( NULL::text AS result_content, NULL::uuid AS source_id FROM follow_up_task f JOIN patient p ON f.patient_id = p.id - WHERE f.tenant_id = $1 AND f.deleted_at IS NULL {fu_status}"# + WHERE f.tenant_id = $1 AND f.deleted_at IS NULL {fu_status} {assigned_filter} {patient_filter}"# )); } @@ -792,8 +817,9 @@ pub struct TeamOverview { pub async fn get_workbench_stats( db: &DatabaseConnection, tenant_id: Uuid, + user_id: Option, ) -> Result { - tracing::info!(tenant_id = %tenant_id, "获取工作台统计"); + tracing::info!(tenant_id = %tenant_id, user_id = ?user_id, "获取工作台统计"); let ai_pending: i64 = FromQueryResult::find_by_statement( Statement::from_sql_and_values( DatabaseBackend::Postgres, @@ -826,10 +852,17 @@ pub async fn get_workbench_stats( .map(|r: CountRow| r.cnt) .unwrap_or(0); + let followup_due_sql = match user_id { + Some(uid) => format!( + "SELECT COUNT(*) AS cnt FROM follow_up_task WHERE tenant_id = $1 AND status = 'pending' AND deleted_at IS NULL AND assigned_to = '{}'", + uid + ), + None => "SELECT COUNT(*) AS cnt FROM follow_up_task WHERE tenant_id = $1 AND status = 'pending' AND deleted_at IS NULL".to_string(), + }; let followup_due: i64 = FromQueryResult::find_by_statement( Statement::from_sql_and_values( DatabaseBackend::Postgres, - "SELECT COUNT(*) AS cnt FROM follow_up_task WHERE tenant_id = $1 AND status = 'pending' AND deleted_at IS NULL", + followup_due_sql, [tenant_id.into()], ), ) @@ -965,3 +998,63 @@ pub async fn get_team_overview( total_completed, }) } + +/// 获取护士班次患者列表 — 今日分配给该护士的随访任务中涉及的患者 +pub async fn get_nurse_patients( + db: &DatabaseConnection, + tenant_id: Uuid, + user_id: Uuid, +) -> Result, HealthError> { + #[derive(Debug, FromQueryResult)] + struct PatientRow { + patient_id: Uuid, + patient_name: String, + pending_actions: i64, + highest_severity: Option, + } + + let rows: Vec = FromQueryResult::find_by_statement( + Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT f.patient_id, p.name AS patient_name, + COUNT(*) AS pending_actions, + MAX(COALESCE( + (SELECT al.severity FROM alerts al + WHERE al.patient_id = f.patient_id AND al.tenant_id = $1 + AND al.status = 'active' AND al.deleted_at IS NULL + ORDER BY al.created_at DESC LIMIT 1), + 'low' + )) AS highest_severity + FROM follow_up_task f + JOIN patient p ON f.patient_id = p.id + WHERE f.tenant_id = $1 + AND f.assigned_to = $2 + AND f.status IN ('pending', 'in_progress') + AND f.deleted_at IS NULL + AND f.planned_date <= CURRENT_DATE + GROUP BY f.patient_id, p.name + ORDER BY pending_actions DESC + "#, + [tenant_id.into(), user_id.into()], + ), + ) + .all(db) + .await + .map_err(|e| { + tracing::error!(tenant_id = %tenant_id, user_id = %user_id, error = %e, "查询护士班次患者失败"); + HealthError::DbError(e.to_string()) + })?; + + let summaries = rows + .into_iter() + .map(|r| NursePatientSummary { + patient_id: r.patient_id, + patient_name: r.patient_name, + pending_actions: r.pending_actions, + highest_priority: risk_to_priority(r.highest_severity.as_deref().unwrap_or("low")), + }) + .collect(); + + Ok(summaries) +}