- Apply rustfmt to all Rust source files in backend/ - Fix trailing whitespace inconsistencies - Standardize formatting across handlers, models, and services - Improve code readability with consistent formatting These changes are purely stylistic and do not affect functionality. All CI checks now pass with proper formatting.
268 lines
8.2 KiB
Rust
268 lines
8.2 KiB
Rust
use crate::auth::jwt::Claims;
|
|
use crate::config::AppState;
|
|
use crate::models::health_stats::HealthStatistic;
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Extension, Json,
|
|
};
|
|
use mongodb::bson::oid::ObjectId;
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateHealthStatRequest {
|
|
pub stat_type: String,
|
|
pub value: serde_json::Value, // Support complex values like blood pressure
|
|
pub unit: String,
|
|
pub notes: Option<String>,
|
|
pub recorded_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateHealthStatRequest {
|
|
pub value: Option<serde_json::Value>,
|
|
pub unit: Option<String>,
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct HealthTrendsQuery {
|
|
pub stat_type: String,
|
|
pub period: Option<String>, // "7d", "30d", etc.
|
|
}
|
|
|
|
pub async fn create_health_stat(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Json(req): Json<CreateHealthStatRequest>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
|
|
// Convert complex value to f64 or store as string
|
|
let value_num = match req.value {
|
|
serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
|
|
serde_json::Value::Object(_) => {
|
|
// For complex objects like blood pressure, use a default
|
|
0.0
|
|
}
|
|
_ => 0.0,
|
|
};
|
|
|
|
let stat = HealthStatistic {
|
|
id: None,
|
|
user_id: claims.sub.clone(),
|
|
stat_type: req.stat_type,
|
|
value: value_num,
|
|
unit: req.unit,
|
|
notes: req.notes,
|
|
recorded_at: req.recorded_at.unwrap_or_else(|| {
|
|
use chrono::Utc;
|
|
Utc::now().to_rfc3339()
|
|
}),
|
|
};
|
|
|
|
match repo.create(&stat).await {
|
|
Ok(created) => (StatusCode::CREATED, Json(created)).into_response(),
|
|
Err(e) => {
|
|
eprintln!("Error creating health stat: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to create health stat",
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn list_health_stats(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
match repo.find_by_user(&claims.sub).await {
|
|
Ok(stats) => (StatusCode::OK, Json(stats)).into_response(),
|
|
Err(e) => {
|
|
eprintln!("Error fetching health stats: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to fetch health stats",
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_health_stat(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Path(id): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
let object_id = match ObjectId::parse_str(&id) {
|
|
Ok(oid) => oid,
|
|
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid ID").into_response(),
|
|
};
|
|
|
|
match repo.find_by_id(&object_id).await {
|
|
Ok(Some(stat)) => {
|
|
if stat.user_id != claims.sub {
|
|
return (StatusCode::FORBIDDEN, "Access denied").into_response();
|
|
}
|
|
(StatusCode::OK, Json(stat)).into_response()
|
|
}
|
|
Ok(None) => (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
|
|
Err(e) => {
|
|
eprintln!("Error fetching health stat: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to fetch health stat",
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn update_health_stat(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Path(id): Path<String>,
|
|
Json(req): Json<UpdateHealthStatRequest>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
let object_id = match ObjectId::parse_str(&id) {
|
|
Ok(oid) => oid,
|
|
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid ID").into_response(),
|
|
};
|
|
|
|
let mut stat = match repo.find_by_id(&object_id).await {
|
|
Ok(Some(s)) => s,
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to fetch health stat",
|
|
)
|
|
.into_response()
|
|
}
|
|
};
|
|
|
|
if stat.user_id != claims.sub {
|
|
return (StatusCode::FORBIDDEN, "Access denied").into_response();
|
|
}
|
|
|
|
if let Some(value) = req.value {
|
|
let value_num = match value {
|
|
serde_json::Value::Number(n) => n.as_f64().unwrap_or(0.0),
|
|
_ => 0.0,
|
|
};
|
|
stat.value = value_num;
|
|
}
|
|
if let Some(unit) = req.unit {
|
|
stat.unit = unit;
|
|
}
|
|
if let Some(notes) = req.notes {
|
|
stat.notes = Some(notes);
|
|
}
|
|
|
|
match repo.update(&object_id, &stat).await {
|
|
Ok(Some(updated)) => (StatusCode::OK, Json(updated)).into_response(),
|
|
Ok(None) => (StatusCode::NOT_FOUND, "Failed to update").into_response(),
|
|
Err(_) => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to update health stat",
|
|
)
|
|
.into_response(),
|
|
}
|
|
}
|
|
|
|
pub async fn delete_health_stat(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Path(id): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
let object_id = match ObjectId::parse_str(&id) {
|
|
Ok(oid) => oid,
|
|
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid ID").into_response(),
|
|
};
|
|
|
|
let stat = match repo.find_by_id(&object_id).await {
|
|
Ok(Some(s)) => s,
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Health stat not found").into_response(),
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to fetch health stat",
|
|
)
|
|
.into_response()
|
|
}
|
|
};
|
|
|
|
if stat.user_id != claims.sub {
|
|
return (StatusCode::FORBIDDEN, "Access denied").into_response();
|
|
}
|
|
|
|
match repo.delete(&object_id).await {
|
|
Ok(true) => StatusCode::NO_CONTENT.into_response(),
|
|
_ => (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to delete health stat",
|
|
)
|
|
.into_response(),
|
|
}
|
|
}
|
|
|
|
pub async fn get_health_trends(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Query(query): Query<HealthTrendsQuery>,
|
|
) -> impl IntoResponse {
|
|
let repo = state.health_stats_repo.as_ref().unwrap();
|
|
match repo.find_by_user(&claims.sub).await {
|
|
Ok(stats) => {
|
|
// Filter by stat_type
|
|
let filtered: Vec<HealthStatistic> = stats
|
|
.into_iter()
|
|
.filter(|s| s.stat_type == query.stat_type)
|
|
.collect();
|
|
|
|
// Calculate basic trend statistics
|
|
if filtered.is_empty() {
|
|
return (
|
|
StatusCode::OK,
|
|
Json(serde_json::json!({
|
|
"stat_type": query.stat_type,
|
|
"count": 0,
|
|
"data": []
|
|
})),
|
|
)
|
|
.into_response();
|
|
}
|
|
|
|
let values: Vec<f64> = filtered.iter().map(|s| s.value).collect();
|
|
let avg = values.iter().sum::<f64>() / values.len() as f64;
|
|
let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
|
|
let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
|
|
|
|
let response = serde_json::json!({
|
|
"stat_type": query.stat_type,
|
|
"count": filtered.len(),
|
|
"average": avg,
|
|
"min": min,
|
|
"max": max,
|
|
"data": filtered
|
|
});
|
|
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error fetching health trends: {:?}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to fetch health trends",
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
}
|