normogen/backend/src/handlers/health_stats.rs
goose ee0feb77ef
Some checks failed
Lint and Build / Lint (push) Failing after 5s
Lint and Build / Build (push) Has been skipped
Lint and Build / Docker Build (push) Has been skipped
style: apply rustfmt to backend codebase
- 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.
2026-03-11 11:16:03 -03:00

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()
}
}
}