Display global statistics
This commit is contained in:
		@@ -8,6 +8,7 @@ pub mod files_controller;
 | 
				
			|||||||
pub mod movement_controller;
 | 
					pub mod movement_controller;
 | 
				
			||||||
pub mod server_controller;
 | 
					pub mod server_controller;
 | 
				
			||||||
pub mod static_controller;
 | 
					pub mod static_controller;
 | 
				
			||||||
 | 
					pub mod stats_controller;
 | 
				
			||||||
pub mod tokens_controller;
 | 
					pub mod tokens_controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(thiserror::Error, Debug)]
 | 
					#[derive(thiserror::Error, Debug)]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										25
									
								
								moneymgr_backend/src/controllers/stats_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								moneymgr_backend/src/controllers/stats_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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),
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -155,6 +155,8 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/api/movement/{movement_id}",
 | 
					                "/api/movement/{movement_id}",
 | 
				
			||||||
                web::delete().to(movement_controller::delete),
 | 
					                web::delete().to(movement_controller::delete),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            // Statistics controller
 | 
				
			||||||
 | 
					            .route("/api/stats/global", web::get().to(stats_controller::global))
 | 
				
			||||||
            // Static assets
 | 
					            // Static assets
 | 
				
			||||||
            .route("/", web::get().to(static_controller::root_index))
 | 
					            .route("/", web::get().to(static_controller::root_index))
 | 
				
			||||||
            .route(
 | 
					            .route(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
use crate::models::accounts::AccountID;
 | 
					use crate::models::accounts::AccountID;
 | 
				
			||||||
use crate::models::files::FileID;
 | 
					use crate::models::files::FileID;
 | 
				
			||||||
use crate::schema::*;
 | 
					use crate::schema::*;
 | 
				
			||||||
use diesel::{Insertable, Queryable};
 | 
					use diesel::{Insertable, Queryable, QueryableByName};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 | 
					#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
 | 
				
			||||||
pub struct MovementID(pub i32);
 | 
					pub struct MovementID(pub i32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Queryable, Debug, Clone, serde::Serialize)]
 | 
					#[derive(Queryable, QueryableByName, Debug, Clone, serde::Serialize)]
 | 
				
			||||||
pub struct Movement {
 | 
					pub struct Movement {
 | 
				
			||||||
    id: i32,
 | 
					    id: i32,
 | 
				
			||||||
    account_id: i32,
 | 
					    account_id: i32,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,6 +112,13 @@ pub async fn delete_file_if_unused(id: FileID) -> anyhow::Result<bool> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get all the files of a user
 | 
				
			||||||
 | 
					pub async fn get_all_files_user(user_id: UserID) -> anyhow::Result<Vec<File>> {
 | 
				
			||||||
 | 
					    Ok(files::table
 | 
				
			||||||
 | 
					        .filter(files::dsl::user_id.eq(user_id.0))
 | 
				
			||||||
 | 
					        .load(&mut db()?)?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get the entire list of file
 | 
					/// Get the entire list of file
 | 
				
			||||||
pub async fn get_entire_list() -> anyhow::Result<Vec<File>> {
 | 
					pub async fn get_entire_list() -> anyhow::Result<Vec<File>> {
 | 
				
			||||||
    Ok(files::table.get_results(&mut db()?)?)
 | 
					    Ok(files::table.get_results(&mut db()?)?)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -162,6 +162,17 @@ pub async fn get_balances(user_id: UserID) -> anyhow::Result<HashMap<AccountID,
 | 
				
			|||||||
        .collect())
 | 
					        .collect())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get all the movements of the user
 | 
				
			||||||
 | 
					pub async fn get_all_movements_user(user_id: UserID) -> anyhow::Result<Vec<Movement>> {
 | 
				
			||||||
 | 
					    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::<Movement>(&mut db()?)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(movements)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Delete a movement
 | 
					/// Delete a movement
 | 
				
			||||||
pub async fn delete(id: MovementID) -> anyhow::Result<()> {
 | 
					pub async fn delete(id: MovementID) -> anyhow::Result<()> {
 | 
				
			||||||
    diesel::delete(movements::dsl::movements.filter(movements::dsl::id.eq(id.0)))
 | 
					    diesel::delete(movements::dsl::movements.filter(movements::dsl::id.eq(id.0)))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								moneymgr_web/src/api/StatsApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								moneymgr_web/src/api/StatsApi.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<GlobalStats> {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      await APIClient.exec({
 | 
				
			||||||
 | 
					        uri: `/stats/global`,
 | 
				
			||||||
 | 
					        method: "GET",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    ).data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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 {
 | 
					export function HomeRoute(): React.ReactElement {
 | 
				
			||||||
  const accounts = useAccounts();
 | 
					  const loadKey = React.useRef(1);
 | 
				
			||||||
  console.log(accounts.list.list);
 | 
					  const [global, setGlobal] = React.useState<GlobalStats | undefined>();
 | 
				
			||||||
  return <>home authenticated todo</>;
 | 
					
 | 
				
			||||||
 | 
					  const load = async () => {
 | 
				
			||||||
 | 
					    setGlobal(await StatsApi.GetGlobal());
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const reload = async () => {
 | 
				
			||||||
 | 
					    loadKey.current += 1;
 | 
				
			||||||
 | 
					    setGlobal(undefined);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <MoneyMgrWebRouteContainer
 | 
				
			||||||
 | 
					      label={"Welcome to Money Manager"}
 | 
				
			||||||
 | 
					      actions={
 | 
				
			||||||
 | 
					        <Tooltip title="Refresh dashboard">
 | 
				
			||||||
 | 
					          <IconButton onClick={reload}>
 | 
				
			||||||
 | 
					            <RefreshIcon />
 | 
				
			||||||
 | 
					          </IconButton>
 | 
				
			||||||
 | 
					        </Tooltip>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <AsyncWidget
 | 
				
			||||||
 | 
					        ready={global !== undefined}
 | 
				
			||||||
 | 
					        loadKey={loadKey.current}
 | 
				
			||||||
 | 
					        load={load}
 | 
				
			||||||
 | 
					        errMsg="Failed to load statistics!"
 | 
				
			||||||
 | 
					        build={() => <StatsDashboard global={global!} />}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </MoneyMgrWebRouteContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function StatsDashboard(p: { global: GlobalStats }): React.ReactElement {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Grid container spacing={2}>
 | 
				
			||||||
 | 
					        {" "}
 | 
				
			||||||
 | 
					        <StatTile
 | 
				
			||||||
 | 
					          title="Global balance"
 | 
				
			||||||
 | 
					          value={<AmountWidget amount={p.global.global_balance} />}
 | 
				
			||||||
 | 
					          icon={<FunctionsIcon />}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <StatTile
 | 
				
			||||||
 | 
					          title="Number of movements"
 | 
				
			||||||
 | 
					          value={p.global.number_movements}
 | 
				
			||||||
 | 
					          icon={<ImportExportIcon />}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <StatTile
 | 
				
			||||||
 | 
					          title="Number of files"
 | 
				
			||||||
 | 
					          value={p.global.number_files}
 | 
				
			||||||
 | 
					          icon={<FolderCopyIcon />}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <StatTile
 | 
				
			||||||
 | 
					          title="Total size of files"
 | 
				
			||||||
 | 
					          value={filesize(p.global.total_files_size)}
 | 
				
			||||||
 | 
					          icon={<ScaleIcon />}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Grid>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function StatTile(p: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  value: number | string | React.ReactElement;
 | 
				
			||||||
 | 
					  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>
 | 
				
			||||||
 | 
					      </Paper>
 | 
				
			||||||
 | 
					    </Grid>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user