627 lines
22 KiB
Rust
627 lines
22 KiB
Rust
use async_trait::async_trait;
|
||
use chrono::Datelike;
|
||
use erp_core::crypto::{self as pii, PiiCrypto};
|
||
use erp_core::error::{AppError, AppResult};
|
||
use erp_core::health_provider::{
|
||
AnomalyInfo, AppointmentSummaryDto, HealthDataProvider, HealthReportDto, LabItemDto,
|
||
LabReportDto, MedicationSummaryDto, MetricTrendAnalysis, PatientSummaryDto, RegressionStats,
|
||
ReportSectionDto, TimeRange, TrendAnalysisDto, TrendDirection, VitalSignDto,
|
||
};
|
||
use num_traits::ToPrimitive;
|
||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
|
||
use uuid::Uuid;
|
||
|
||
use crate::entity::{
|
||
appointment, diagnosis, health_record, lab_report, medication_record, patient, vital_signs,
|
||
};
|
||
|
||
pub struct HealthDataProviderImpl {
|
||
pub db: sea_orm::DatabaseConnection,
|
||
pub crypto: PiiCrypto,
|
||
}
|
||
|
||
fn compute_age_group(birth_date: Option<chrono::NaiveDate>) -> String {
|
||
let Some(bd) = birth_date else {
|
||
return "未知".to_string();
|
||
};
|
||
let age = chrono::Utc::now().date_naive().year() - bd.year();
|
||
match age {
|
||
a if a < 14 => "儿童",
|
||
a if a < 36 => "青年",
|
||
a if a < 56 => "中年",
|
||
_ => "老年",
|
||
}
|
||
.to_string()
|
||
}
|
||
|
||
async fn find_patient(
|
||
db: &sea_orm::DatabaseConnection,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
) -> AppResult<patient::Model> {
|
||
patient::Entity::find_by_id(patient_id)
|
||
.filter(patient::Column::TenantId.eq(tenant_id))
|
||
.filter(patient::Column::DeletedAt.is_null())
|
||
.one(db)
|
||
.await?
|
||
.ok_or_else(|| AppError::NotFound(format!("患者 {patient_id} 不存在")))
|
||
}
|
||
|
||
async fn find_lab_report(
|
||
db: &sea_orm::DatabaseConnection,
|
||
tenant_id: Uuid,
|
||
report_id: Uuid,
|
||
) -> AppResult<lab_report::Model> {
|
||
lab_report::Entity::find_by_id(report_id)
|
||
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
||
.filter(lab_report::Column::DeletedAt.is_null())
|
||
.one(db)
|
||
.await?
|
||
.ok_or_else(|| AppError::NotFound(format!("化验报告 {report_id} 不存在")))
|
||
}
|
||
|
||
fn parse_lab_items(items_json: &Option<serde_json::Value>) -> Vec<LabItemDto> {
|
||
let Some(arr) = items_json.as_ref().and_then(|v| v.as_array()) else {
|
||
return vec![];
|
||
};
|
||
arr.iter()
|
||
.filter_map(|item| {
|
||
// 兼容两种存储格式:item_name/name
|
||
let name = item
|
||
.get("item_name")
|
||
.or_else(|| item.get("name"))?
|
||
.as_str()?
|
||
.to_string();
|
||
|
||
// 兼容 value 为字符串或数字
|
||
let value = item
|
||
.get("value")
|
||
.and_then(|v| v.as_f64())
|
||
.or_else(|| item.get("value")?.as_str()?.parse::<f64>().ok())
|
||
.unwrap_or(0.0);
|
||
|
||
let unit = item
|
||
.get("unit")
|
||
.and_then(|v| v.as_str())
|
||
.unwrap_or("")
|
||
.to_string();
|
||
|
||
// 兼容两种参考范围格式:reference_range(string) 或 reference_low/reference_high(number)
|
||
let reference_range = item
|
||
.get("reference_range")
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.to_string())
|
||
.unwrap_or_else(|| {
|
||
let low = item
|
||
.get("reference_low")
|
||
.and_then(|v| v.as_f64())
|
||
.map(|l| l.to_string());
|
||
let high = item
|
||
.get("reference_high")
|
||
.and_then(|v| v.as_f64())
|
||
.map(|h| h.to_string());
|
||
match (low, high) {
|
||
(Some(l), Some(h)) => format!("{l}-{h}"),
|
||
(Some(l), None) => format!(">={l}"),
|
||
(None, Some(h)) => format!("<={h}"),
|
||
_ => "-".to_string(),
|
||
}
|
||
});
|
||
|
||
let is_abnormal = item
|
||
.get("is_abnormal")
|
||
.and_then(|v| v.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
Some(LabItemDto {
|
||
name,
|
||
value,
|
||
unit,
|
||
reference_range,
|
||
is_abnormal,
|
||
})
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn report_type_to_department(report_type: &str) -> &str {
|
||
match report_type {
|
||
"kidney_function" => "肾内科",
|
||
"blood_routine" => "血液科",
|
||
"electrolyte" => "检验科",
|
||
"liver_function" => "肝胆科",
|
||
_ => "检验科",
|
||
}
|
||
}
|
||
|
||
#[async_trait]
|
||
impl HealthDataProvider for HealthDataProviderImpl {
|
||
async fn get_lab_report(&self, tenant_id: Uuid, report_id: Uuid) -> AppResult<LabReportDto> {
|
||
let report = find_lab_report(&self.db, tenant_id, report_id).await?;
|
||
let patient = find_patient(&self.db, tenant_id, report.patient_id).await?;
|
||
|
||
// 解密 items:加密时存储为 Value::String(ciphertext)
|
||
let kek = self.crypto.kek();
|
||
let decrypted_items = report
|
||
.items
|
||
.as_ref()
|
||
.and_then(|v| v.as_str())
|
||
.and_then(|s| pii::decrypt(kek, s).ok())
|
||
.and_then(|s| serde_json::from_str(&s).ok())
|
||
.or(report.items.clone());
|
||
|
||
Ok(LabReportDto {
|
||
age_group: compute_age_group(patient.birth_date),
|
||
sex: patient.gender.unwrap_or_else(|| "未知".to_string()),
|
||
department: report_type_to_department(&report.report_type).to_string(),
|
||
report_date: report.report_date.to_string(),
|
||
items: parse_lab_items(&decrypted_items),
|
||
})
|
||
}
|
||
|
||
async fn get_vital_signs(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
metrics: &[String],
|
||
range: &TimeRange,
|
||
) -> AppResult<Vec<VitalSignDto>> {
|
||
let _ = find_patient(&self.db, tenant_id, patient_id).await?;
|
||
|
||
let start_date = range.start.date_naive();
|
||
let end_date = range.end.date_naive();
|
||
|
||
let records = vital_signs::Entity::find()
|
||
.filter(vital_signs::Column::TenantId.eq(tenant_id))
|
||
.filter(vital_signs::Column::PatientId.eq(patient_id))
|
||
.filter(vital_signs::Column::DeletedAt.is_null())
|
||
.filter(vital_signs::Column::RecordDate.gte(start_date))
|
||
.filter(vital_signs::Column::RecordDate.lte(end_date))
|
||
.order_by_asc(vital_signs::Column::RecordDate)
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
let metric_extractors: [(&str, Box<dyn Fn(&vital_signs::Model) -> Option<f64>>); 8] = [
|
||
(
|
||
"systolic_bp_morning",
|
||
Box::new(|r| r.systolic_bp_morning.map(|v| v as f64)),
|
||
),
|
||
(
|
||
"diastolic_bp_morning",
|
||
Box::new(|r| r.diastolic_bp_morning.map(|v| v as f64)),
|
||
),
|
||
("heart_rate", Box::new(|r| r.heart_rate.map(|v| v as f64))),
|
||
(
|
||
"weight",
|
||
Box::new(|r| r.weight.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
(
|
||
"blood_sugar",
|
||
Box::new(|r| r.blood_sugar.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
(
|
||
"body_temperature",
|
||
Box::new(|r| r.body_temperature.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
("spo2", Box::new(|r| r.spo2.map(|v| v as f64))),
|
||
(
|
||
"urine_output_ml",
|
||
Box::new(|r| r.urine_output_ml.map(|v| v as f64)),
|
||
),
|
||
];
|
||
|
||
let mut result = Vec::new();
|
||
|
||
for (metric_name, extractor) in &metric_extractors {
|
||
if !metrics.is_empty() && !metrics.iter().any(|m| m == *metric_name) {
|
||
continue;
|
||
}
|
||
|
||
let values: Vec<(String, f64)> = records
|
||
.iter()
|
||
.filter_map(|r| extractor(r).map(|v| (r.record_date.to_string(), v)))
|
||
.collect();
|
||
|
||
if values.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
let unit = match *metric_name {
|
||
"systolic_bp_morning" | "diastolic_bp_morning" => "mmHg",
|
||
"heart_rate" => "bpm",
|
||
"weight" => "kg",
|
||
"blood_sugar" => "mmol/L",
|
||
"body_temperature" => "°C",
|
||
"spo2" => "%",
|
||
"urine_output_ml" => "ml",
|
||
_ => "",
|
||
};
|
||
|
||
result.push(VitalSignDto {
|
||
metric: metric_name.to_string(),
|
||
values,
|
||
unit: unit.to_string(),
|
||
});
|
||
}
|
||
|
||
Ok(result)
|
||
}
|
||
|
||
async fn get_patient_summary(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
) -> AppResult<PatientSummaryDto> {
|
||
let patient = find_patient(&self.db, tenant_id, patient_id).await?;
|
||
|
||
let diagnoses: Vec<String> = diagnosis::Entity::find()
|
||
.filter(diagnosis::Column::TenantId.eq(tenant_id))
|
||
.filter(diagnosis::Column::PatientId.eq(patient_id))
|
||
.filter(diagnosis::Column::DeletedAt.is_null())
|
||
.filter(diagnosis::Column::Status.eq("active"))
|
||
.order_by_desc(diagnosis::Column::DiagnosedDate)
|
||
.all(&self.db)
|
||
.await?
|
||
.iter()
|
||
.map(|d| format!("{}({})", d.diagnosis_name, d.icd_code))
|
||
.collect();
|
||
|
||
let medications: Vec<String> = medication_record::Entity::find()
|
||
.filter(medication_record::Column::TenantId.eq(tenant_id))
|
||
.filter(medication_record::Column::PatientId.eq(patient_id))
|
||
.filter(medication_record::Column::DeletedAt.is_null())
|
||
.filter(medication_record::Column::IsCurrent.eq(true))
|
||
.all(&self.db)
|
||
.await?
|
||
.iter()
|
||
.map(|m| {
|
||
let mut s = m.medication_name.clone();
|
||
if let Some(ref dosage) = m.dosage {
|
||
s.push_str(&format!(" {dosage}"));
|
||
}
|
||
s
|
||
})
|
||
.collect();
|
||
|
||
let family_history = patient
|
||
.medical_history_summary
|
||
.as_ref()
|
||
.map(|h| {
|
||
h.split(';')
|
||
.chain(h.split(';'))
|
||
.map(|s| s.trim().to_string())
|
||
.filter(|s| !s.is_empty())
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
let last_checkup = health_record::Entity::find()
|
||
.filter(health_record::Column::TenantId.eq(tenant_id))
|
||
.filter(health_record::Column::PatientId.eq(patient_id))
|
||
.filter(health_record::Column::DeletedAt.is_null())
|
||
.order_by_desc(health_record::Column::RecordDate)
|
||
.one(&self.db)
|
||
.await?;
|
||
|
||
let last_checkup_date = last_checkup
|
||
.map(|r| r.record_date.to_string())
|
||
.unwrap_or_else(|| "无".to_string());
|
||
|
||
Ok(PatientSummaryDto {
|
||
age_group: compute_age_group(patient.birth_date),
|
||
sex: patient.gender.unwrap_or_else(|| "未知".to_string()),
|
||
chronic_conditions: diagnoses,
|
||
medications,
|
||
family_history,
|
||
last_checkup_date,
|
||
})
|
||
}
|
||
|
||
async fn get_full_report(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
report_id: Uuid,
|
||
) -> AppResult<HealthReportDto> {
|
||
let record = health_record::Entity::find_by_id(report_id)
|
||
.filter(health_record::Column::TenantId.eq(tenant_id))
|
||
.filter(health_record::Column::DeletedAt.is_null())
|
||
.one(&self.db)
|
||
.await?
|
||
.ok_or_else(|| AppError::NotFound(format!("健康报告 {report_id} 不存在")))?;
|
||
|
||
let patient = find_patient(&self.db, tenant_id, record.patient_id).await?;
|
||
|
||
let mut sections = Vec::new();
|
||
|
||
let findings: Vec<String> = record
|
||
.overall_assessment
|
||
.as_ref()
|
||
.map(|a| {
|
||
a.split(';')
|
||
.chain(a.split(';'))
|
||
.map(|s| s.trim().to_string())
|
||
.filter(|s| !s.is_empty())
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
if !findings.is_empty() {
|
||
sections.push(ReportSectionDto {
|
||
title: "总体评估".to_string(),
|
||
findings,
|
||
abnormal_items: vec![],
|
||
});
|
||
}
|
||
|
||
let report_diagnoses = diagnosis::Entity::find()
|
||
.filter(diagnosis::Column::TenantId.eq(tenant_id))
|
||
.filter(diagnosis::Column::PatientId.eq(record.patient_id))
|
||
.filter(diagnosis::Column::DeletedAt.is_null())
|
||
.filter(
|
||
diagnosis::Column::HealthRecordId
|
||
.eq(report_id)
|
||
.or(diagnosis::Column::Status.eq("active")),
|
||
)
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
if !report_diagnoses.is_empty() {
|
||
let (abnormal, findings): (Vec<_>, Vec<_>) =
|
||
report_diagnoses.iter().partition(|d| d.status == "active");
|
||
sections.push(ReportSectionDto {
|
||
title: "诊断记录".to_string(),
|
||
findings: findings
|
||
.iter()
|
||
.map(|d| {
|
||
format!(
|
||
"{}({}) — {}",
|
||
d.diagnosis_name, d.icd_code, d.diagnosed_date
|
||
)
|
||
})
|
||
.collect(),
|
||
abnormal_items: abnormal
|
||
.iter()
|
||
.map(|d| format!("{}({})", d.diagnosis_name, d.icd_code))
|
||
.collect(),
|
||
});
|
||
}
|
||
|
||
let lab_reports = lab_report::Entity::find()
|
||
.filter(lab_report::Column::TenantId.eq(tenant_id))
|
||
.filter(lab_report::Column::PatientId.eq(record.patient_id))
|
||
.filter(lab_report::Column::DeletedAt.is_null())
|
||
.filter(
|
||
lab_report::Column::ReportDate.gte(record.record_date - chrono::Duration::days(30)),
|
||
)
|
||
.filter(lab_report::Column::ReportDate.lte(record.record_date))
|
||
.order_by_desc(lab_report::Column::ReportDate)
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
for lr in &lab_reports {
|
||
let items = parse_lab_items(&lr.items);
|
||
let abnormal: Vec<String> = items
|
||
.iter()
|
||
.filter(|i| i.is_abnormal)
|
||
.map(|i| format!("{} {}{}", i.name, i.value, i.unit))
|
||
.collect();
|
||
let findings: Vec<String> = items
|
||
.iter()
|
||
.map(|i| format!("{}: {}{} ({})", i.name, i.value, i.unit, i.reference_range))
|
||
.collect();
|
||
if !findings.is_empty() {
|
||
sections.push(ReportSectionDto {
|
||
title: format!("化验报告 — {} ({})", lr.report_type, lr.report_date),
|
||
findings,
|
||
abnormal_items: abnormal,
|
||
});
|
||
}
|
||
}
|
||
|
||
Ok(HealthReportDto {
|
||
age_group: compute_age_group(patient.birth_date),
|
||
sex: patient.gender.unwrap_or_else(|| "未知".to_string()),
|
||
department: record.record_type.clone(),
|
||
report_date: record.record_date.to_string(),
|
||
sections,
|
||
})
|
||
}
|
||
|
||
async fn get_trend_analysis_data(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
metrics: &[String],
|
||
range: &TimeRange,
|
||
) -> AppResult<TrendAnalysisDto> {
|
||
let _ = find_patient(&self.db, tenant_id, patient_id).await?;
|
||
|
||
let start_date = range.start.date_naive();
|
||
let end_date = range.end.date_naive();
|
||
|
||
let records = vital_signs::Entity::find()
|
||
.filter(vital_signs::Column::TenantId.eq(tenant_id))
|
||
.filter(vital_signs::Column::PatientId.eq(patient_id))
|
||
.filter(vital_signs::Column::DeletedAt.is_null())
|
||
.filter(vital_signs::Column::RecordDate.gte(start_date))
|
||
.filter(vital_signs::Column::RecordDate.lte(end_date))
|
||
.order_by_asc(vital_signs::Column::RecordDate)
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
let metric_extractors: [(&str, Box<dyn Fn(&vital_signs::Model) -> Option<f64>>); 8] = [
|
||
(
|
||
"systolic_bp_morning",
|
||
Box::new(|r| r.systolic_bp_morning.map(|v| v as f64)),
|
||
),
|
||
(
|
||
"diastolic_bp_morning",
|
||
Box::new(|r| r.diastolic_bp_morning.map(|v| v as f64)),
|
||
),
|
||
("heart_rate", Box::new(|r| r.heart_rate.map(|v| v as f64))),
|
||
(
|
||
"weight",
|
||
Box::new(|r| r.weight.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
(
|
||
"blood_sugar",
|
||
Box::new(|r| r.blood_sugar.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
(
|
||
"body_temperature",
|
||
Box::new(|r| r.body_temperature.map(|v| v.to_f64().unwrap_or(0.0))),
|
||
),
|
||
("spo2", Box::new(|r| r.spo2.map(|v| v as f64))),
|
||
(
|
||
"urine_output_ml",
|
||
Box::new(|r| r.urine_output_ml.map(|v| v as f64)),
|
||
),
|
||
];
|
||
|
||
let mut metric_results = Vec::new();
|
||
|
||
for (metric_name, extractor) in &metric_extractors {
|
||
if !metrics.is_empty() && !metrics.iter().any(|m| m == *metric_name) {
|
||
continue;
|
||
}
|
||
|
||
// 构建时间序列数据
|
||
let time_series: Vec<(chrono::NaiveDate, f64)> = records
|
||
.iter()
|
||
.filter_map(|r| extractor(r).map(|v| (r.record_date, v)))
|
||
.collect();
|
||
|
||
let data_point_count = time_series.len();
|
||
if data_point_count == 0 {
|
||
continue;
|
||
}
|
||
|
||
// 线性回归
|
||
let regression = crate::service::trend_stats::compute_linear_regression(&time_series)
|
||
.map(|r| RegressionStats {
|
||
slope: r.slope,
|
||
intercept: r.intercept,
|
||
r_squared: r.r_squared,
|
||
direction: match r.direction {
|
||
crate::service::trend_stats::TrendDirection::Rising => {
|
||
TrendDirection::Rising
|
||
}
|
||
crate::service::trend_stats::TrendDirection::Falling => {
|
||
TrendDirection::Falling
|
||
}
|
||
crate::service::trend_stats::TrendDirection::Stable => {
|
||
TrendDirection::Stable
|
||
}
|
||
},
|
||
daily_change: r.daily_change,
|
||
period_change: r.period_change,
|
||
});
|
||
|
||
// 异常检测(使用 2.0 倍标准差阈值)
|
||
let anomaly_points = crate::service::trend_stats::detect_anomalies(&time_series, 2.0);
|
||
let anomalies: Vec<AnomalyInfo> = anomaly_points
|
||
.into_iter()
|
||
.map(|a| AnomalyInfo {
|
||
date: a.date.to_string(),
|
||
value: a.value,
|
||
mean: a.mean,
|
||
std_dev: a.std_dev,
|
||
deviation: a.deviation,
|
||
})
|
||
.collect();
|
||
|
||
let unit = match *metric_name {
|
||
"systolic_bp_morning" | "diastolic_bp_morning" => "mmHg",
|
||
"heart_rate" => "bpm",
|
||
"weight" => "kg",
|
||
"blood_sugar" => "mmol/L",
|
||
"body_temperature" => "°C",
|
||
"spo2" => "%",
|
||
"urine_output_ml" => "ml",
|
||
_ => "",
|
||
};
|
||
|
||
metric_results.push(MetricTrendAnalysis {
|
||
metric: metric_name.to_string(),
|
||
unit: unit.to_string(),
|
||
data_point_count,
|
||
regression,
|
||
anomalies,
|
||
});
|
||
}
|
||
|
||
Ok(TrendAnalysisDto {
|
||
patient_id,
|
||
period_start: start_date.to_string(),
|
||
period_end: end_date.to_string(),
|
||
metrics: metric_results,
|
||
})
|
||
}
|
||
|
||
async fn get_upcoming_appointments(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
) -> AppResult<Vec<AppointmentSummaryDto>> {
|
||
let _ = find_patient(&self.db, tenant_id, patient_id).await?;
|
||
|
||
let today = chrono::Utc::now().date_naive();
|
||
let records = appointment::Entity::find()
|
||
.filter(appointment::Column::TenantId.eq(tenant_id))
|
||
.filter(appointment::Column::PatientId.eq(patient_id))
|
||
.filter(appointment::Column::DeletedAt.is_null())
|
||
.filter(appointment::Column::AppointmentDate.gte(today))
|
||
.filter(
|
||
appointment::Column::Status
|
||
.is_in(vec!["scheduled".to_string(), "confirmed".to_string()]),
|
||
)
|
||
.order_by_asc(appointment::Column::AppointmentDate)
|
||
.order_by_asc(appointment::Column::StartTime)
|
||
.limit(10)
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
let result = records
|
||
.into_iter()
|
||
.map(|r| AppointmentSummaryDto {
|
||
id: r.id,
|
||
department: r.appointment_type,
|
||
doctor_name: r
|
||
.doctor_id
|
||
.map_or("待定".to_string(), |_| "医生".to_string()),
|
||
scheduled_at: format!("{} {}", r.appointment_date, r.start_time),
|
||
status: r.status,
|
||
})
|
||
.collect();
|
||
|
||
Ok(result)
|
||
}
|
||
|
||
async fn get_medication_list(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
patient_id: Uuid,
|
||
) -> AppResult<Vec<MedicationSummaryDto>> {
|
||
let _ = find_patient(&self.db, tenant_id, patient_id).await?;
|
||
|
||
let records = medication_record::Entity::find()
|
||
.filter(medication_record::Column::TenantId.eq(tenant_id))
|
||
.filter(medication_record::Column::PatientId.eq(patient_id))
|
||
.filter(medication_record::Column::DeletedAt.is_null())
|
||
.filter(medication_record::Column::IsCurrent.eq(true))
|
||
.all(&self.db)
|
||
.await?;
|
||
|
||
let result = records
|
||
.into_iter()
|
||
.map(|m| MedicationSummaryDto {
|
||
name: m.medication_name,
|
||
dosage: m.dosage.unwrap_or_default(),
|
||
frequency: m.frequency.unwrap_or_default(),
|
||
})
|
||
.collect();
|
||
|
||
Ok(result)
|
||
}
|
||
}
|