From 272c8ab3128b14f6168a3eafa3badab90432adcf Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 2 May 2025 10:01:22 +0200 Subject: [PATCH] Display global statistics --- moneymgr_backend/src/controllers/mod.rs | 1 + .../src/controllers/stats_controller.rs | 25 +++++ moneymgr_backend/src/main.rs | 2 + moneymgr_backend/src/models/movements.rs | 4 +- .../src/services/files_service.rs | 7 ++ .../src/services/movements_service.rs | 11 ++ moneymgr_web/src/api/StatsApi.ts | 22 ++++ moneymgr_web/src/routes/HomeRoute.tsx | 104 +++++++++++++++++- 8 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 moneymgr_backend/src/controllers/stats_controller.rs create mode 100644 moneymgr_web/src/api/StatsApi.ts diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 1bb254c..21d305c 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -8,6 +8,7 @@ pub mod files_controller; pub mod movement_controller; pub mod server_controller; pub mod static_controller; +pub mod stats_controller; pub mod tokens_controller; #[derive(thiserror::Error, Debug)] diff --git a/moneymgr_backend/src/controllers/stats_controller.rs b/moneymgr_backend/src/controllers/stats_controller.rs new file mode 100644 index 0000000..e5dac09 --- /dev/null +++ b/moneymgr_backend/src/controllers/stats_controller.rs @@ -0,0 +1,25 @@ +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::AuthExtractor; +use crate::services::{files_service, movements_service}; +use actix_web::HttpResponse; + +#[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), + })) +} diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index eb77dc3..38acc22 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -155,6 +155,8 @@ async fn main() -> std::io::Result<()> { "/api/movement/{movement_id}", web::delete().to(movement_controller::delete), ) + // Statistics controller + .route("/api/stats/global", web::get().to(stats_controller::global)) // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/moneymgr_backend/src/models/movements.rs b/moneymgr_backend/src/models/movements.rs index ba0a738..2f0f01f 100644 --- a/moneymgr_backend/src/models/movements.rs +++ b/moneymgr_backend/src/models/movements.rs @@ -1,12 +1,12 @@ use crate::models::accounts::AccountID; use crate::models::files::FileID; use crate::schema::*; -use diesel::{Insertable, Queryable}; +use diesel::{Insertable, Queryable, QueryableByName}; #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct MovementID(pub i32); -#[derive(Queryable, Debug, Clone, serde::Serialize)] +#[derive(Queryable, QueryableByName, Debug, Clone, serde::Serialize)] pub struct Movement { id: i32, account_id: i32, diff --git a/moneymgr_backend/src/services/files_service.rs b/moneymgr_backend/src/services/files_service.rs index 505fa23..cc21939 100644 --- a/moneymgr_backend/src/services/files_service.rs +++ b/moneymgr_backend/src/services/files_service.rs @@ -112,6 +112,13 @@ pub async fn delete_file_if_unused(id: FileID) -> anyhow::Result { } } +/// Get all the files of a user +pub async fn get_all_files_user(user_id: UserID) -> anyhow::Result> { + Ok(files::table + .filter(files::dsl::user_id.eq(user_id.0)) + .load(&mut db()?)?) +} + /// Get the entire list of file pub async fn get_entire_list() -> anyhow::Result> { Ok(files::table.get_results(&mut db()?)?) diff --git a/moneymgr_backend/src/services/movements_service.rs b/moneymgr_backend/src/services/movements_service.rs index fba4deb..e8e49a1 100644 --- a/moneymgr_backend/src/services/movements_service.rs +++ b/moneymgr_backend/src/services/movements_service.rs @@ -162,6 +162,17 @@ pub async fn get_balances(user_id: UserID) -> anyhow::Result anyhow::Result> { + let movements = sql_query(format!( + "select m.* from movements m join accounts a on a.id = m.account_id where a.user_id = {}", + user_id.0 + )) + .load::(&mut db()?)?; + + Ok(movements) +} + /// Delete a movement pub async fn delete(id: MovementID) -> anyhow::Result<()> { diesel::delete(movements::dsl::movements.filter(movements::dsl::id.eq(id.0))) diff --git a/moneymgr_web/src/api/StatsApi.ts b/moneymgr_web/src/api/StatsApi.ts new file mode 100644 index 0000000..8dbff0a --- /dev/null +++ b/moneymgr_web/src/api/StatsApi.ts @@ -0,0 +1,22 @@ +import { APIClient } from "./ApiClient"; + +export interface GlobalStats { + global_balance: number; + number_movements: number; + number_files: number; + total_files_size: number; +} + +export class StatsApi { + /** + * Get global statistics + */ + static async GetGlobal(): Promise { + return ( + await APIClient.exec({ + uri: `/stats/global`, + method: "GET", + }) + ).data; + } +} diff --git a/moneymgr_web/src/routes/HomeRoute.tsx b/moneymgr_web/src/routes/HomeRoute.tsx index f80d791..814dff1 100644 --- a/moneymgr_web/src/routes/HomeRoute.tsx +++ b/moneymgr_web/src/routes/HomeRoute.tsx @@ -1,7 +1,103 @@ -import { useAccounts } from "../hooks/AccountsListProvider"; +import FunctionsIcon from "@mui/icons-material/Functions"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { Grid, IconButton, Paper, Tooltip, Typography } from "@mui/material"; +import React from "react"; +import { GlobalStats, StatsApi } from "../api/StatsApi"; +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 accounts = useAccounts(); - console.log(accounts.list.list); - return <>home authenticated todo; + const loadKey = React.useRef(1); + const [global, setGlobal] = React.useState(); + + const load = async () => { + setGlobal(await StatsApi.GetGlobal()); + }; + + const reload = async () => { + loadKey.current += 1; + setGlobal(undefined); + }; + + return ( + + + + + + } + > + } + /> + + ); +} + +export function StatsDashboard(p: { global: GlobalStats }): React.ReactElement { + return ( + <> + + {" "} + } + icon={} + /> + } + /> + } + /> + } + /> + + + ); +} + +export function StatTile(p: { + title: string; + value: number | string | React.ReactElement; + icon: React.ReactElement; +}): React.ReactElement { + return ( + + +
+ {p.icon} + + {p.title} + + {p.value} +
+
+
+ ); }