Display global statistics
This commit is contained in:
parent
16ef1147fe
commit
272c8ab312
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user