feat: implement health statistics tracking (Phase 2.7 Task 2)
- Add HealthStatistics model with 10 stat types - Implement HealthStatisticsRepository - Create 6 health stats API endpoints - Add trend analysis with summary calculations - Follow medication repository pattern Status: 60% complete, needs compilation fixes
This commit is contained in:
parent
d673415bc6
commit
b59be78e4a
18 changed files with 2420 additions and 7 deletions
246
backend/src/models/health_stats.rs
Normal file
246
backend/src/models/health_stats.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use mongodb::{bson::{oid::ObjectId, doc}, Collection, DateTime};
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthStatistic {
|
||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<ObjectId>,
|
||||
#[serde(rename = "healthStatId")]
|
||||
pub health_stat_id: String,
|
||||
#[serde(rename = "userId")]
|
||||
pub user_id: String,
|
||||
#[serde(rename = "profileId")]
|
||||
pub profile_id: String,
|
||||
#[serde(rename = "statType")]
|
||||
pub stat_type: HealthStatType,
|
||||
#[serde(rename = "value")]
|
||||
pub value: HealthStatValue,
|
||||
#[serde(rename = "unit")]
|
||||
pub unit: String,
|
||||
#[serde(rename = "recordedAt")]
|
||||
pub recorded_at: DateTime,
|
||||
#[serde(rename = "notes")]
|
||||
pub notes: Option<String>,
|
||||
#[serde(rename = "tags")]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(rename = "createdAt")]
|
||||
pub created_at: DateTime,
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum HealthStatType {
|
||||
Weight,
|
||||
Height,
|
||||
BloodPressure,
|
||||
HeartRate,
|
||||
Temperature,
|
||||
BloodGlucose,
|
||||
OxygenSaturation,
|
||||
SleepHours,
|
||||
Steps,
|
||||
Calories,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl HealthStatType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
HealthStatType::Weight => "weight",
|
||||
HealthStatType::Height => "height",
|
||||
HealthStatType::BloodPressure => "blood_pressure",
|
||||
HealthStatType::HeartRate => "heart_rate",
|
||||
HealthStatType::Temperature => "temperature",
|
||||
HealthStatType::BloodGlucose => "blood_glucose",
|
||||
HealthStatType::OxygenSaturation => "oxygen_saturation",
|
||||
HealthStatType::SleepHours => "sleep_hours",
|
||||
HealthStatType::Steps => "steps",
|
||||
HealthStatType::Calories => "calories",
|
||||
HealthStatType::Custom(name) => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_unit(&self) -> &str {
|
||||
match self {
|
||||
HealthStatType::Weight => "kg",
|
||||
HealthStatType::Height => "cm",
|
||||
HealthStatType::BloodPressure => "mmHg",
|
||||
HealthStatType::HeartRate => "bpm",
|
||||
HealthStatType::Temperature => "°C",
|
||||
HealthStatType::BloodGlucose => "mg/dL",
|
||||
HealthStatType::OxygenSaturation => "%",
|
||||
HealthStatType::SleepHours => "hours",
|
||||
HealthStatType::Steps => "steps",
|
||||
HealthStatType::Calories => "kcal",
|
||||
HealthStatType::Custom(_) => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum HealthStatValue {
|
||||
Single(f64),
|
||||
BloodPressure { systolic: f64, diastolic: f64 },
|
||||
String(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateHealthStatRequest {
|
||||
pub profile_id: String,
|
||||
#[serde(rename = "statType")]
|
||||
pub stat_type: String,
|
||||
pub value: serde_json::Value,
|
||||
pub unit: Option<String>,
|
||||
#[serde(rename = "recordedAt")]
|
||||
pub recorded_at: Option<DateTime>,
|
||||
pub notes: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateHealthStatRequest {
|
||||
pub value: Option<serde_json::Value>,
|
||||
pub unit: Option<String>,
|
||||
#[serde(rename = "recordedAt")]
|
||||
pub recorded_at: Option<DateTime>,
|
||||
pub notes: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HealthStatisticsRepository {
|
||||
pub collection: Collection<HealthStatistic>,
|
||||
}
|
||||
|
||||
impl HealthStatisticsRepository {
|
||||
pub fn new(collection: Collection<HealthStatistic>) -> Self {
|
||||
Self { collection }
|
||||
}
|
||||
|
||||
pub async fn create(&self, stat: HealthStatistic) -> Result<HealthStatistic, Box<dyn std::error::Error>> {
|
||||
self.collection.insert_one(stat.clone(), None).await?;
|
||||
Ok(stat)
|
||||
}
|
||||
|
||||
pub async fn list_by_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
stat_type: Option<&str>,
|
||||
profile_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<HealthStatistic>, Box<dyn std::error::Error>> {
|
||||
let mut filter = doc! {
|
||||
"userId": user_id
|
||||
};
|
||||
|
||||
if let Some(stat_type) = stat_type {
|
||||
filter.insert("statType", stat_type);
|
||||
}
|
||||
|
||||
if let Some(profile_id) = profile_id {
|
||||
filter.insert("profileId", profile_id);
|
||||
}
|
||||
|
||||
let find_options = mongodb::options::FindOptions::builder()
|
||||
.sort(doc! { "recordedAt": -1 })
|
||||
.limit(limit)
|
||||
.build();
|
||||
|
||||
let cursor = self.collection.find(filter, find_options).await?;
|
||||
let results: Vec<_> = cursor.try_collect().await?;
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn get_by_id(&self, id: &ObjectId, user_id: &str) -> Result<Option<HealthStatistic>, Box<dyn std::error::Error>> {
|
||||
let filter = doc! {
|
||||
"_id": id,
|
||||
"userId": user_id
|
||||
};
|
||||
let result = self.collection.find_one(filter, None).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
id: &ObjectId,
|
||||
user_id: &str,
|
||||
update: UpdateHealthStatRequest,
|
||||
) -> Result<Option<HealthStatistic>, Box<dyn std::error::Error>> {
|
||||
let filter = doc! {
|
||||
"_id": id,
|
||||
"userId": user_id
|
||||
};
|
||||
|
||||
let mut update_doc = doc! {};
|
||||
|
||||
if let Some(value) = update.value {
|
||||
update_doc.insert("value", mongodb::bson::to_bson(&value)?);
|
||||
}
|
||||
if let Some(unit) = update.unit {
|
||||
update_doc.insert("unit", unit);
|
||||
}
|
||||
if let Some(recorded_at) = update.recorded_at {
|
||||
update_doc.insert("recordedAt", recorded_at);
|
||||
}
|
||||
if let Some(notes) = update.notes {
|
||||
update_doc.insert("notes", notes);
|
||||
}
|
||||
if let Some(tags) = update.tags {
|
||||
update_doc.insert("tags", tags);
|
||||
}
|
||||
|
||||
update_doc.insert("updatedAt", DateTime::now());
|
||||
|
||||
let update = doc! {
|
||||
"$set": update_doc
|
||||
};
|
||||
|
||||
let result = self.collection.update_one(filter, update, None).await?;
|
||||
if result.modified_count > 0 {
|
||||
self.get_by_id(id, user_id).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: &ObjectId, user_id: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let filter = doc! {
|
||||
"_id": id,
|
||||
"userId": user_id
|
||||
};
|
||||
let result = self.collection.delete_one(filter, None).await?;
|
||||
Ok(result.deleted_count > 0)
|
||||
}
|
||||
|
||||
pub async fn get_trends(
|
||||
&self,
|
||||
user_id: &str,
|
||||
profile_id: &str,
|
||||
stat_type: &str,
|
||||
days: i64,
|
||||
) -> Result<Vec<HealthStatistic>, Box<dyn std::error::Error>> {
|
||||
// Use chrono duration instead of DateTime arithmetic
|
||||
let now = chrono::Utc::now();
|
||||
let days_ago = now - chrono::Duration::days(days);
|
||||
let days_ago_bson = DateTime::from_chrono(days_ago);
|
||||
|
||||
let filter = doc! {
|
||||
"userId": user_id,
|
||||
"profileId": profile_id,
|
||||
"statType": stat_type,
|
||||
"recordedAt": { "$gte": days_ago_bson }
|
||||
};
|
||||
|
||||
let find_options = mongodb::options::FindOptions::builder()
|
||||
.sort(doc! { "recordedAt": 1 })
|
||||
.build();
|
||||
|
||||
let cursor = self.collection.find(filter, find_options).await?;
|
||||
let results: Vec<_> = cursor.try_collect().await?;
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
pub mod user;
|
||||
pub mod audit_log;
|
||||
pub mod family;
|
||||
pub mod profile;
|
||||
pub mod health_data;
|
||||
pub mod health_stats;
|
||||
pub mod lab_result;
|
||||
pub mod medication;
|
||||
pub mod appointment;
|
||||
pub mod share;
|
||||
pub mod permission;
|
||||
pub mod profile;
|
||||
pub mod refresh_token;
|
||||
pub mod session;
|
||||
pub mod audit_log;
|
||||
pub mod share;
|
||||
pub mod user;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue