Display balance evolution chart
This commit is contained in:
		@@ -1,7 +1,10 @@
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::extractors::auth_extractor::AuthExtractor;
 | 
			
		||||
use crate::services::{files_service, movements_service};
 | 
			
		||||
use actix_web::HttpResponse;
 | 
			
		||||
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 {
 | 
			
		||||
@@ -23,3 +26,75 @@ pub async fn global(auth: AuthExtractor) -> HttpResult {
 | 
			
		||||
        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<u64>,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    start: Option<i64>,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    end: Option<i64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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<String, f32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<BalanceVariationQuery>,
 | 
			
		||||
) -> 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Final push
 | 
			
		||||
    dataset.push(stat_entry);
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(dataset))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -157,6 +157,10 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
            )
 | 
			
		||||
            // Statistics controller
 | 
			
		||||
            .route("/api/stats/global", web::get().to(stats_controller::global))
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/stats/balance_variation",
 | 
			
		||||
                web::get().to(stats_controller::balance_variation),
 | 
			
		||||
            )
 | 
			
		||||
            // Static assets
 | 
			
		||||
            .route("/", web::get().to(static_controller::root_index))
 | 
			
		||||
            .route(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,28 +6,41 @@ use diesel::{Insertable, Queryable, QueryableByName};
 | 
			
		||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 | 
			
		||||
pub struct MovementID(pub i32);
 | 
			
		||||
 | 
			
		||||
/// Single movement information
 | 
			
		||||
#[derive(Queryable, QueryableByName, Debug, Clone, serde::Serialize)]
 | 
			
		||||
pub struct Movement {
 | 
			
		||||
    /// The ID of the movement
 | 
			
		||||
    id: i32,
 | 
			
		||||
    /// The ID of the account this movement is attached to
 | 
			
		||||
    account_id: i32,
 | 
			
		||||
    /// The time this movement happened
 | 
			
		||||
    pub time: i64,
 | 
			
		||||
    /// The label associated to this movement
 | 
			
		||||
    pub label: String,
 | 
			
		||||
    /// ID of the file attached to this movement (if any)
 | 
			
		||||
    file_id: Option<i32>,
 | 
			
		||||
    /// The amount of the movement
 | 
			
		||||
    pub amount: f32,
 | 
			
		||||
    /// Checkbox presented to the user (no impact on code logic)
 | 
			
		||||
    pub checked: bool,
 | 
			
		||||
    /// The time this movement was created in the database
 | 
			
		||||
    pub time_create: i64,
 | 
			
		||||
    /// The time this movement was last updated in the database
 | 
			
		||||
    pub time_update: i64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Movement {
 | 
			
		||||
    /// Get the ID of the movement
 | 
			
		||||
    pub fn id(&self) -> MovementID {
 | 
			
		||||
        MovementID(self.id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// The ID of the account attached to the movement
 | 
			
		||||
    pub fn account_id(&self) -> AccountID {
 | 
			
		||||
        AccountID(self.account_id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// The ID of the file attached to the movement, if any
 | 
			
		||||
    pub fn file_id(&self) -> Option<FileID> {
 | 
			
		||||
        self.file_id.map(FileID)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user