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)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										325
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										325
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -16,6 +16,7 @@
 | 
			
		||||
        "@mdi/react": "^1.6.1",
 | 
			
		||||
        "@mui/icons-material": "^7.0.1",
 | 
			
		||||
        "@mui/material": "^7.0.1",
 | 
			
		||||
        "@mui/x-charts": "^8.2.0",
 | 
			
		||||
        "@mui/x-data-grid": "^7.28.3",
 | 
			
		||||
        "@mui/x-date-pickers": "^8.0.0-beta.3",
 | 
			
		||||
        "date-and-time": "^3.6.0",
 | 
			
		||||
@@ -1541,12 +1542,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/types": {
 | 
			
		||||
      "version": "7.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-TxJ4ezEeedWHBjOmLtxI203a9DII9l4k83RXmz1PYSAmnyEcK2PglTNmJGxswC/wM5cdl9ap2h8lnXvt2swAGQ==",
 | 
			
		||||
      "version": "7.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.26.10"
 | 
			
		||||
        "@babel/runtime": "^7.27.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
			
		||||
@@ -1558,17 +1559,17 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/utils": {
 | 
			
		||||
      "version": "7.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-SJKrrebNpmK9rJCnVL29nGPhPXQYtBZmb7Dsp0f58uIUhQfAKcBXHE4Kjs06SX4CwqeCuwEVgcHY+MgAO6XQ/g==",
 | 
			
		||||
      "version": "7.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.26.10",
 | 
			
		||||
        "@mui/types": "^7.4.0",
 | 
			
		||||
        "@babel/runtime": "^7.27.0",
 | 
			
		||||
        "@mui/types": "^7.4.1",
 | 
			
		||||
        "@types/prop-types": "^15.7.14",
 | 
			
		||||
        "clsx": "^2.1.1",
 | 
			
		||||
        "prop-types": "^15.8.1",
 | 
			
		||||
        "react-is": "^19.0.0"
 | 
			
		||||
        "react-is": "^19.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.0.0"
 | 
			
		||||
@@ -1587,6 +1588,87 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts": {
 | 
			
		||||
      "version": "8.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Onf9ZrZmoTz3awrOKXtMDHqTXroGSdDJismIVQP71MHEcoOB+qvNDaekUJqkx8jGQwldTQnwDpkzXCQaiX9RRg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.0",
 | 
			
		||||
        "@mui/utils": "^7.0.2",
 | 
			
		||||
        "@mui/x-charts-vendor": "8.0.0",
 | 
			
		||||
        "@mui/x-internals": "8.2.0",
 | 
			
		||||
        "bezier-easing": "^2.1.0",
 | 
			
		||||
        "clsx": "^2.1.1",
 | 
			
		||||
        "prop-types": "^15.8.1",
 | 
			
		||||
        "reselect": "^5.1.1",
 | 
			
		||||
        "use-sync-external-store": "^1.5.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@emotion/react": "^11.9.0",
 | 
			
		||||
        "@emotion/styled": "^11.8.1",
 | 
			
		||||
        "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
 | 
			
		||||
        "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
 | 
			
		||||
        "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
 | 
			
		||||
        "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "@emotion/react": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "@emotion/styled": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts-vendor": {
 | 
			
		||||
      "version": "8.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-aXv0QlCTkVxSNX+sHdG92jaQMEWJFw2NuxBx599JyZ5Ij038JwdU9x0dArfPdtpdCX0A19lHKHYgZ8S0I4LpnQ==",
 | 
			
		||||
      "license": "MIT AND ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.0",
 | 
			
		||||
        "@types/d3-color": "^3.1.3",
 | 
			
		||||
        "@types/d3-delaunay": "^6.0.4",
 | 
			
		||||
        "@types/d3-interpolate": "^3.0.4",
 | 
			
		||||
        "@types/d3-scale": "^4.0.9",
 | 
			
		||||
        "@types/d3-shape": "^3.1.7",
 | 
			
		||||
        "@types/d3-time": "^3.0.4",
 | 
			
		||||
        "@types/d3-timer": "^3.0.2",
 | 
			
		||||
        "d3-color": "^3.1.0",
 | 
			
		||||
        "d3-delaunay": "^6.0.4",
 | 
			
		||||
        "d3-interpolate": "^3.0.1",
 | 
			
		||||
        "d3-scale": "^4.0.2",
 | 
			
		||||
        "d3-shape": "^3.2.0",
 | 
			
		||||
        "d3-time": "^3.1.0",
 | 
			
		||||
        "d3-timer": "^3.0.1",
 | 
			
		||||
        "delaunator": "^5.0.1",
 | 
			
		||||
        "robust-predicates": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-charts/node_modules/@mui/x-internals": {
 | 
			
		||||
      "version": "8.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-qV4Qr+m4sAPBSuqu8/Ofi5m+nMMvIybGno6cp757bHSmwxkqrn5SKaGyFnH5kB58fOhYA9hG1UivFp7mO1dE4A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.27.0",
 | 
			
		||||
        "@mui/utils": "^7.0.2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "opencollective",
 | 
			
		||||
        "url": "https://opencollective.com/mui-org"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mui/x-data-grid": {
 | 
			
		||||
      "version": "7.28.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.28.3.tgz",
 | 
			
		||||
@@ -2096,6 +2178,63 @@
 | 
			
		||||
      "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-color": {
 | 
			
		||||
      "version": "3.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-delaunay": {
 | 
			
		||||
      "version": "6.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-interpolate": {
 | 
			
		||||
      "version": "3.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/d3-color": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-path": {
 | 
			
		||||
      "version": "3.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
 | 
			
		||||
      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-scale": {
 | 
			
		||||
      "version": "4.0.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
 | 
			
		||||
      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/d3-time": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-shape": {
 | 
			
		||||
      "version": "3.1.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
 | 
			
		||||
      "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/d3-path": "*"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-time": {
 | 
			
		||||
      "version": "3.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/d3-timer": {
 | 
			
		||||
      "version": "3.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/estree": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
 | 
			
		||||
@@ -2474,6 +2613,12 @@
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/bezier-easing": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/birecord": {
 | 
			
		||||
      "version": "0.1.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/birecord/-/birecord-0.1.1.tgz",
 | 
			
		||||
@@ -2690,6 +2835,130 @@
 | 
			
		||||
      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-array": {
 | 
			
		||||
      "version": "3.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
 | 
			
		||||
      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "internmap": "1 - 2"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-color": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-delaunay": {
 | 
			
		||||
      "version": "6.0.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
 | 
			
		||||
      "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "delaunator": "5"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-format": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-interpolate": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3-color": "1 - 3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-path": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-scale": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3-array": "2.10.0 - 3",
 | 
			
		||||
        "d3-format": "1 - 3",
 | 
			
		||||
        "d3-interpolate": "1.2.0 - 3",
 | 
			
		||||
        "d3-time": "2.1.1 - 3",
 | 
			
		||||
        "d3-time-format": "2 - 4"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-shape": {
 | 
			
		||||
      "version": "3.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3-path": "^3.1.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-time": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3-array": "2 - 3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-time-format": {
 | 
			
		||||
      "version": "4.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3-time": "1 - 3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/d3-timer": {
 | 
			
		||||
      "version": "3.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/date-and-time": {
 | 
			
		||||
      "version": "3.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.6.0.tgz",
 | 
			
		||||
@@ -2726,6 +2995,15 @@
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/delaunator": {
 | 
			
		||||
      "version": "5.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "robust-predicates": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/dom-helpers": {
 | 
			
		||||
      "version": "5.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
 | 
			
		||||
@@ -3355,6 +3633,15 @@
 | 
			
		||||
        "node": ">=0.8.19"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/internmap": {
 | 
			
		||||
      "version": "2.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
 | 
			
		||||
      "license": "ISC",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-arrayish": {
 | 
			
		||||
      "version": "0.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
 | 
			
		||||
@@ -3928,9 +4215,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-is": {
 | 
			
		||||
      "version": "19.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
 | 
			
		||||
      "version": "19.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/react-refresh": {
 | 
			
		||||
@@ -4051,6 +4338,12 @@
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/robust-predicates": {
 | 
			
		||||
      "version": "3.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
 | 
			
		||||
      "license": "Unlicense"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/rollup": {
 | 
			
		||||
      "version": "4.36.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz",
 | 
			
		||||
@@ -4390,9 +4683,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/use-sync-external-store": {
 | 
			
		||||
      "version": "1.4.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
 | 
			
		||||
      "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
    "@mdi/react": "^1.6.1",
 | 
			
		||||
    "@mui/icons-material": "^7.0.1",
 | 
			
		||||
    "@mui/material": "^7.0.1",
 | 
			
		||||
    "@mui/x-charts": "^8.2.0",
 | 
			
		||||
    "@mui/x-data-grid": "^7.28.3",
 | 
			
		||||
    "@mui/x-date-pickers": "^8.0.0-beta.3",
 | 
			
		||||
    "date-and-time": "^3.6.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { APIClient } from "./ApiClient";
 | 
			
		||||
 | 
			
		||||
type Balances = Record<number, number>;
 | 
			
		||||
export type Balances = Record<number, number>;
 | 
			
		||||
 | 
			
		||||
export interface MovementUpdate {
 | 
			
		||||
  account_id: number;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ export interface GlobalStats {
 | 
			
		||||
  total_files_size: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type StatBalanceVariation = Record<string, number> & { time: number };
 | 
			
		||||
 | 
			
		||||
export class StatsApi {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get global statistics
 | 
			
		||||
@@ -19,4 +21,20 @@ export class StatsApi {
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get balance variation statistics
 | 
			
		||||
   */
 | 
			
		||||
  static async BalanceVariationStats(
 | 
			
		||||
    start: number,
 | 
			
		||||
    end: number,
 | 
			
		||||
    interval: number
 | 
			
		||||
  ): Promise<StatBalanceVariation[]> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        uri: `/stats/balance_variation?start=${start}&end=${end}&interval=${interval}`,
 | 
			
		||||
        method: "GET",
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,49 @@
 | 
			
		||||
import FolderCopyIcon from "@mui/icons-material/FolderCopy";
 | 
			
		||||
import FunctionsIcon from "@mui/icons-material/Functions";
 | 
			
		||||
import ImportExportIcon from "@mui/icons-material/ImportExport";
 | 
			
		||||
import RefreshIcon from "@mui/icons-material/Refresh";
 | 
			
		||||
import ScaleIcon from "@mui/icons-material/Scale";
 | 
			
		||||
import { Grid, IconButton, Paper, Tooltip, Typography } from "@mui/material";
 | 
			
		||||
import { LineChart } from "@mui/x-charts/LineChart";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { GlobalStats, StatsApi } from "../api/StatsApi";
 | 
			
		||||
import { GlobalStats, StatBalanceVariation, StatsApi } from "../api/StatsApi";
 | 
			
		||||
import { useAccounts } from "../hooks/AccountsListProvider";
 | 
			
		||||
import { fmtDateFromTime, time } from "../utils/DateUtils";
 | 
			
		||||
import { AmountWidget } from "../widgets/AmountWidget";
 | 
			
		||||
import { AsyncWidget } from "../widgets/AsyncWidget";
 | 
			
		||||
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
 | 
			
		||||
import ImportExportIcon from "@mui/icons-material/ImportExport";
 | 
			
		||||
import FolderCopyIcon from "@mui/icons-material/FolderCopy";
 | 
			
		||||
import ScaleIcon from "@mui/icons-material/Scale";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import { AmountWidget } from "../widgets/AmountWidget";
 | 
			
		||||
 | 
			
		||||
export function HomeRoute(): React.ReactElement {
 | 
			
		||||
  const account = useAccounts();
 | 
			
		||||
 | 
			
		||||
  const loadKey = React.useRef(1);
 | 
			
		||||
 | 
			
		||||
  const [global, setGlobal] = React.useState<GlobalStats | undefined>();
 | 
			
		||||
  const [lastYear, setLastYear] = React.useState<
 | 
			
		||||
    StatBalanceVariation[] | undefined
 | 
			
		||||
  >();
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setGlobal(await StatsApi.GetGlobal());
 | 
			
		||||
    const lastyear = await StatsApi.BalanceVariationStats(
 | 
			
		||||
      time() - 3600 * 24 * 365,
 | 
			
		||||
      time(),
 | 
			
		||||
      3600 * 24
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Manually compute sum
 | 
			
		||||
    for (const entry of lastyear) {
 | 
			
		||||
      let sum = 0;
 | 
			
		||||
 | 
			
		||||
      for (const a of account.list.list) {
 | 
			
		||||
        sum += entry[a.id.toString()];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      entry["sum"] = sum;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setLastYear(lastyear);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const reload = async () => {
 | 
			
		||||
@@ -40,17 +67,19 @@ export function HomeRoute(): React.ReactElement {
 | 
			
		||||
        loadKey={loadKey.current}
 | 
			
		||||
        load={load}
 | 
			
		||||
        errMsg="Failed to load statistics!"
 | 
			
		||||
        build={() => <StatsDashboard global={global!} />}
 | 
			
		||||
        build={() => <StatsDashboard global={global!} lastYear={lastYear!} />}
 | 
			
		||||
      />
 | 
			
		||||
    </MoneyMgrWebRouteContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function StatsDashboard(p: { global: GlobalStats }): React.ReactElement {
 | 
			
		||||
export function StatsDashboard(p: {
 | 
			
		||||
  global: GlobalStats;
 | 
			
		||||
  lastYear: StatBalanceVariation[];
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Grid container spacing={2}>
 | 
			
		||||
        {" "}
 | 
			
		||||
        <StatTile
 | 
			
		||||
          title="Global balance"
 | 
			
		||||
          value={<AmountWidget amount={p.global.global_balance} />}
 | 
			
		||||
@@ -72,6 +101,22 @@ export function StatsDashboard(p: { global: GlobalStats }): React.ReactElement {
 | 
			
		||||
          icon={<ScaleIcon />}
 | 
			
		||||
        />
 | 
			
		||||
      </Grid>
 | 
			
		||||
 | 
			
		||||
      <Grid container spacing={2}>
 | 
			
		||||
        <Grid size={6}>
 | 
			
		||||
          <StatChart label="Last week" start={time() - 3600 * 24 * 7} {...p} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid size={6}>
 | 
			
		||||
          <StatChart
 | 
			
		||||
            label="Last month"
 | 
			
		||||
            start={time() - 3600 * 24 * 31}
 | 
			
		||||
            {...p}
 | 
			
		||||
          />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid size={12}>
 | 
			
		||||
          <StatChart label="Last year" {...p} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -82,22 +127,81 @@ export function StatTile(p: {
 | 
			
		||||
  icon: React.ReactElement;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid size={4}>
 | 
			
		||||
      <Paper elevation={5} style={{ padding: "10px" }}>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "row",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {p.icon}
 | 
			
		||||
          <Typography variant="h5" style={{ flex: 1 }}>
 | 
			
		||||
            {p.title}
 | 
			
		||||
          </Typography>
 | 
			
		||||
          {p.value}
 | 
			
		||||
        </div>
 | 
			
		||||
    <Grid size={{ sm: 6, md: 4, lg: 3 }}>
 | 
			
		||||
      <Paper
 | 
			
		||||
        elevation={5}
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          flexDirection: "row",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          padding: "10px",
 | 
			
		||||
          height: "80%",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {p.icon}
 | 
			
		||||
        <Typography variant="h6" style={{ margin: "0em 1em", flex: 1 }}>
 | 
			
		||||
          {p.title}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        {p.value}
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function StatChart(p: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  start?: number;
 | 
			
		||||
  lastYear: StatBalanceVariation[];
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const accounts = useAccounts();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ marginBottom: "50px", marginTop: "20px" }}>
 | 
			
		||||
      <Typography
 | 
			
		||||
        variant="subtitle1"
 | 
			
		||||
        style={{ textAlign: "center", fontWeight: "bold" }}
 | 
			
		||||
      >
 | 
			
		||||
        {p.label}
 | 
			
		||||
      </Typography>
 | 
			
		||||
      <LineChart
 | 
			
		||||
        dataset={p.lastYear}
 | 
			
		||||
        yAxis={[
 | 
			
		||||
          {
 | 
			
		||||
            width: 70,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        xAxis={[
 | 
			
		||||
          {
 | 
			
		||||
            id: "Date",
 | 
			
		||||
            dataKey: "time",
 | 
			
		||||
            scaleType: "time",
 | 
			
		||||
            min: p.start,
 | 
			
		||||
            valueFormatter: fmtDateFromTime,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        series={[
 | 
			
		||||
          {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            label: "Total",
 | 
			
		||||
            dataKey: "sum",
 | 
			
		||||
            stack: "total",
 | 
			
		||||
            area: false,
 | 
			
		||||
            showMark: false,
 | 
			
		||||
          },
 | 
			
		||||
          ...accounts.list.list.map((a) => {
 | 
			
		||||
            return {
 | 
			
		||||
              id: a.id,
 | 
			
		||||
              label: a.name,
 | 
			
		||||
              dataKey: a.id.toString(),
 | 
			
		||||
              stack: "total",
 | 
			
		||||
              area: false,
 | 
			
		||||
              showMark: false,
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
        ]}
 | 
			
		||||
        height={400}
 | 
			
		||||
        hideLegend
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,3 +18,13 @@ export function dateToTime(date: Dayjs | undefined): number | undefined {
 | 
			
		||||
export function time(): number {
 | 
			
		||||
  return Math.floor(new Date().getTime() / 1000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Format date from unix time (secondes since Epoch)
 | 
			
		||||
 */
 | 
			
		||||
export function fmtDateFromTime(time: number): string {
 | 
			
		||||
  const d = new Date(time * 1000);
 | 
			
		||||
  return `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1)
 | 
			
		||||
    .toString()
 | 
			
		||||
    .padStart(2, "0")}/${d.getFullYear()}`;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user