use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::models::accounts::Account; use crate::services::{accounts_service, files_service, movements_service}; use crate::utils::time_utils::time; use actix_web::{HttpResponse, web}; use std::collections::HashMap; #[derive(serde::Serialize)] struct GlobalStats { global_balance: f32, number_movements: usize, number_files: usize, total_files_size: u64, } /// Get global statistics pub async fn global(auth: AuthExtractor) -> HttpResult { let movements = movements_service::get_all_movements_user(auth.user.id()).await?; let files = files_service::get_all_files_user(auth.user.id()).await?; Ok(HttpResponse::Ok().json(GlobalStats { global_balance: movements.iter().fold(0.0, |sum, m| sum + m.amount), number_movements: movements.len(), number_files: files.len(), total_files_size: files.iter().fold(0, |sum, m| sum + m.file_size as u64), })) } #[derive(serde::Deserialize)] pub struct BalanceVariationQuery { #[serde(skip_serializing_if = "Option::is_none")] interval: Option, #[serde(skip_serializing_if = "Option::is_none")] start: Option, #[serde(skip_serializing_if = "Option::is_none")] end: Option, } /// Statistic dataset entry #[derive(serde::Serialize, Debug, Clone)] struct StatEntry { time: i64, /// Account balances. Note: due to JSON limitation, we had to turn account id into strings #[serde(flatten)] balances: HashMap, } impl StatEntry { fn init_first(time: i64, accounts: &[Account]) -> Self { Self { time, balances: accounts .iter() .map(|a| (a.id().0.to_string(), 0.0)) .collect(), } } } /// Accounts balance variation pub async fn balance_variation( auth: AuthExtractor, query: web::Query, ) -> HttpResult { let start = query.start.unwrap_or((time() - 3600 * 24 * 30) as i64); let end = query.end.unwrap_or(time() as i64); let interval = query.interval.unwrap_or(3600 * 24); let accounts = accounts_service::get_list_user(auth.user.id()).await?; let mut movements = movements_service::get_all_movements_user(auth.user.id()).await?; movements.sort_by(|a, b| a.time.cmp(&b.time)); let mut dataset = vec![]; let mut stat_entry = StatEntry::init_first(start, &accounts); // Iter movements for m in movements { if m.time > end { break; } // Check if it is time to go the the next entry while m.time > stat_entry.time { dataset.push(stat_entry.clone()); stat_entry.time += interval as i64; } let target_account_id = m.account_id().0.to_string(); let old_amount = *stat_entry.balances.get(&target_account_id).unwrap_or(&0.0); stat_entry .balances .insert(target_account_id, old_amount + m.amount); } // Add missing end entries, if necessary while stat_entry.time < end { dataset.push(stat_entry.clone()); stat_entry.time += interval as i64; } // Final push dataset.push(stat_entry); Ok(HttpResponse::Ok().json(dataset)) }