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