diff --git a/moneymgr_backend/assets/cash.svg b/moneymgr_backend/assets/cash.svg new file mode 100644 index 0000000..dbdfaa7 --- /dev/null +++ b/moneymgr_backend/assets/cash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/moneymgr_backend/assets/credit-card.svg b/moneymgr_backend/assets/credit-card.svg new file mode 100644 index 0000000..77a1516 --- /dev/null +++ b/moneymgr_backend/assets/credit-card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/moneymgr_backend/assets/saving.svg b/moneymgr_backend/assets/saving.svg new file mode 100644 index 0000000..1ec265a --- /dev/null +++ b/moneymgr_backend/assets/saving.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql b/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql index ee2e8e7..f722c64 100644 --- a/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql +++ b/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql @@ -43,6 +43,7 @@ CREATE TABLE accounts user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, time_create BIGINT NOT NULL, time_update BIGINT NOT NULL, + type VARCHAR(1) NOT NULL DEFAULT 'C', default_account BOOLEAN NOT NULL DEFAULT false ); @@ -52,7 +53,7 @@ CREATE TABLE movements account_id INTEGER NOT NULL REFERENCES accounts ON DELETE CASCADE, time BIGINT NOT NULL, label VARCHAR(200) NOT NULL, - file_id INT REFERENCES files ON DELETE RESTRICT, + file_id INT REFERENCES files ON DELETE RESTRICT, amount REAL NOT NULL, checked BOOLEAN NOT NULL DEFAULT false, time_create BIGINT NOT NULL, diff --git a/moneymgr_backend/src/constants.rs b/moneymgr_backend/src/constants.rs index 8f7845e..25da25a 100644 --- a/moneymgr_backend/src/constants.rs +++ b/moneymgr_backend/src/constants.rs @@ -1,5 +1,7 @@ //! # Project constants +use crate::models::accounts::AccountType; + /// Length of generated tokens pub const TOKENS_LEN: usize = 50; @@ -24,3 +26,30 @@ pub const MAX_UPLOAD_FILE_SIZE: usize = 15 * 1024 * 1024; /// Minimum elapsed time before a file can be deleted by garbage collector (2 hours) pub const MIN_FILE_LIFETIME: u64 = 3600 * 2; + +/// Description of an account type +#[derive(serde::Serialize, Debug)] +pub struct AccountTypeDesc { + label: &'static str, + code: AccountType, + icon: &'static str, +} + +/// Enumeration of accounts types +pub const ACCOUNT_TYPES: [AccountTypeDesc; 3] = [ + AccountTypeDesc { + label: "Cash", + code: AccountType::Cash, + icon: include_str!("../assets/cash.svg"), + }, + AccountTypeDesc { + label: "Bank", + code: AccountType::Bank, + icon: include_str!("../assets/credit-card.svg"), + }, + AccountTypeDesc { + label: "Saving", + code: AccountType::Saving, + icon: include_str!("../assets/saving.svg"), + }, +]; diff --git a/moneymgr_backend/src/controllers/server_controller.rs b/moneymgr_backend/src/controllers/server_controller.rs index 319e811..32a7cb2 100644 --- a/moneymgr_backend/src/controllers/server_controller.rs +++ b/moneymgr_backend/src/controllers/server_controller.rs @@ -1,4 +1,5 @@ use crate::app_config::AppConfig; +use crate::constants::{ACCOUNT_TYPES, AccountTypeDesc}; use actix_web::HttpResponse; /// Serve robots.txt (disallow ranking) @@ -57,6 +58,7 @@ impl Default for ServerConstraints { struct ServerConfig { auth_disabled: bool, oidc_provider_name: &'static str, + accounts_types: &'static [AccountTypeDesc], constraints: ServerConstraints, } @@ -66,6 +68,7 @@ impl Default for ServerConfig { auth_disabled: AppConfig::get().is_auth_disabled(), oidc_provider_name: AppConfig::get().openid_provider().name, constraints: Default::default(), + accounts_types: &ACCOUNT_TYPES, } } } diff --git a/moneymgr_backend/src/models/accounts.rs b/moneymgr_backend/src/models/accounts.rs index e47fa71..3db1075 100644 --- a/moneymgr_backend/src/models/accounts.rs +++ b/moneymgr_backend/src/models/accounts.rs @@ -1,10 +1,31 @@ use crate::models::users::UserID; use crate::schema::*; use diesel::prelude::*; +use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct AccountID(pub i32); +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub enum AccountType { + #[serde(rename = "C")] + Cash, + #[serde(rename = "B")] + Bank, + #[serde(rename = "S")] + Saving, +} + +impl Display for AccountType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + serde_json::to_string(self).unwrap().replace('"', "") + ) + } +} + #[derive(Queryable, Debug, Clone, serde::Serialize)] pub struct Account { id: i32, @@ -12,6 +33,7 @@ pub struct Account { user_id: i32, pub time_create: i64, pub time_update: i64, + r#type: String, pub default_account: bool, } @@ -23,13 +45,22 @@ impl Account { pub fn user_id(&self) -> UserID { UserID(self.user_id) } + + pub fn account_type(&self) -> AccountType { + serde_json::from_str(format!("\"{}\"", self.r#type).as_str()).unwrap() + } + + pub fn set_account_type(&mut self, t: AccountType) { + self.r#type = t.to_string(); + } } -#[derive(Insertable)] +#[derive(Insertable, Debug)] #[diesel(table_name = accounts)] pub struct NewAccount<'a> { pub name: &'a str, pub user_id: i32, pub time_create: i64, pub time_update: i64, + pub type_: String, } diff --git a/moneymgr_backend/src/schema.rs b/moneymgr_backend/src/schema.rs index 6c5b78c..27e1c47 100644 --- a/moneymgr_backend/src/schema.rs +++ b/moneymgr_backend/src/schema.rs @@ -8,6 +8,9 @@ diesel::table! { user_id -> Int4, time_create -> Int8, time_update -> Int8, + #[sql_name = "type"] + #[max_length = 1] + type_ -> Varchar, default_account -> Bool, } } diff --git a/moneymgr_backend/src/services/accounts_service.rs b/moneymgr_backend/src/services/accounts_service.rs index 2a94599..1acd6be 100644 --- a/moneymgr_backend/src/services/accounts_service.rs +++ b/moneymgr_backend/src/services/accounts_service.rs @@ -1,6 +1,6 @@ use crate::connections::db_connection::db; use crate::controllers::server_controller::ServerConstraints; -use crate::models::accounts::{Account, AccountID, NewAccount}; +use crate::models::accounts::{Account, AccountID, AccountType, NewAccount}; use crate::models::users::UserID; use crate::schema::accounts; use crate::utils::time_utils::time; @@ -10,6 +10,7 @@ use diesel::prelude::*; #[derive(serde::Deserialize)] pub struct UpdateAccountQuery { pub name: String, + pub r#type: AccountType, } impl UpdateAccountQuery { @@ -31,6 +32,7 @@ pub async fn create(user_id: UserID, query: &UpdateAccountQuery) -> anyhow::Resu user_id: user_id.0, time_create: time() as i64, time_update: time() as i64, + type_: query.r#type.to_string(), }; let res: Account = diesel::insert_into(accounts::table) @@ -48,6 +50,7 @@ pub async fn update(id: AccountID, q: &UpdateAccountQuery) -> anyhow::Result<()> .set(( accounts::dsl::time_update.eq(time() as i64), accounts::dsl::name.eq(&q.name), + accounts::dsl::type_.eq(&q.r#type.to_string()), )) .execute(&mut db()?)?; diff --git a/moneymgr_web/package-lock.json b/moneymgr_web/package-lock.json index db9995f..79943ac 100644 --- a/moneymgr_web/package-lock.json +++ b/moneymgr_web/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.2.5", + "@jsonjoy.com/base64": "^1.1.2", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.0.1", @@ -1321,6 +1322,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@mdi/js": { "version": "7.4.47", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", @@ -4238,6 +4255,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", diff --git a/moneymgr_web/package.json b/moneymgr_web/package.json index d96bd46..250cfb5 100644 --- a/moneymgr_web/package.json +++ b/moneymgr_web/package.json @@ -13,6 +13,7 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.2.5", + "@jsonjoy.com/base64": "^1.1.2", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.0.1", diff --git a/moneymgr_web/src/api/AccountApi.ts b/moneymgr_web/src/api/AccountApi.ts index dc28a12..cdb5538 100644 --- a/moneymgr_web/src/api/AccountApi.ts +++ b/moneymgr_web/src/api/AccountApi.ts @@ -1,4 +1,5 @@ import { APIClient } from "./ApiClient"; +import { ServerApi } from "./ServerApi"; export interface Account { id: number; @@ -6,6 +7,7 @@ export interface Account { user_id: number; time_create: number; time_update: number; + type: string; default_account: boolean; } @@ -66,6 +68,7 @@ export class AccountApi { method: "POST", jsonData: { name, + type: ServerApi.Config.accounts_types[0].code, }, }); } @@ -79,6 +82,7 @@ export class AccountApi { method: "PUT", jsonData: { name: account.name, + type: account.type, }, }); } diff --git a/moneymgr_web/src/api/ServerApi.ts b/moneymgr_web/src/api/ServerApi.ts index eb047bf..dde8ec5 100644 --- a/moneymgr_web/src/api/ServerApi.ts +++ b/moneymgr_web/src/api/ServerApi.ts @@ -3,9 +3,16 @@ import { APIClient } from "./ApiClient"; export interface ServerConfig { auth_disabled: boolean; oidc_provider_name: string; + accounts_types: AccountType[]; constraints: ServerConstraints; } +export interface AccountType { + label: string; + code: string; + icon: string; +} + export interface ServerConstraints { token_name: LenConstraint; token_ip_net: LenConstraint; diff --git a/moneymgr_web/src/routes/AccountsRoute.tsx b/moneymgr_web/src/routes/AccountsRoute.tsx index 41595e2..5c5061c 100644 --- a/moneymgr_web/src/routes/AccountsRoute.tsx +++ b/moneymgr_web/src/routes/AccountsRoute.tsx @@ -10,6 +10,8 @@ import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer" import { TimeWidget } from "../widgets/TimeWidget"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; +import { AccountWidget } from "../widgets/AccountWidget"; +import { ServerApi } from "../api/ServerApi"; export function AccountsRoute(): React.ReactElement { const alert = useAlert(); @@ -88,6 +90,35 @@ export function AccountsRoute(): React.ReactElement { const columns: GridColDef<(typeof list)[number]>[] = [ { field: "id", headerName: "ID", flex: 1 }, + { + field: "type", + headerName: "Type", + flex: 2, + editable: true, + type: "singleSelect", + valueOptions: ServerApi.Config.accounts_types.map((v) => { + return { label: v.label, value: v.code }; + }), + renderCell(params) { + return ( + + + { + ServerApi.Config.accounts_types.find( + (t) => t.code === params.row.type + )!.label + } + + ); + }, + }, { field: "name", headerName: "Name", flex: 6, editable: true }, { field: "time_create", @@ -159,7 +190,10 @@ export function AccountsRoute(): React.ReactElement { return setDefaultAccount(updated); } - if (updated.name !== original.name) { + if ( + updated.name !== original.name || + updated.type !== original.type + ) { return updateAccount(updated); } diff --git a/moneymgr_web/src/widgets/AccountWidget.tsx b/moneymgr_web/src/widgets/AccountWidget.tsx new file mode 100644 index 0000000..f47e2a8 --- /dev/null +++ b/moneymgr_web/src/widgets/AccountWidget.tsx @@ -0,0 +1,25 @@ +import { Account } from "../api/AccountApi"; +import { ServerApi } from "../api/ServerApi"; +import { useDarkTheme } from "../hooks/context_providers/DarkThemeProvider"; +import { toBase64 } from "@jsonjoy.com/base64"; + +export function AccountWidget(p: { account: Account }): React.ReactElement { + const darkTheme = useDarkTheme(); + + return ( + t.code === p.account.type + )!.icon + ) + )}\")`, + }} + /> + ); +} diff --git a/moneymgr_web/src/widgets/MoneyNavList.tsx b/moneymgr_web/src/widgets/MoneyNavList.tsx index c47dd63..f0e48b2 100644 --- a/moneymgr_web/src/widgets/MoneyNavList.tsx +++ b/moneymgr_web/src/widgets/MoneyNavList.tsx @@ -1,4 +1,4 @@ -import { mdiAccount, mdiApi, mdiCashMultiple, mdiHome } from "@mdi/js"; +import { mdiApi, mdiCashMultiple, mdiHome } from "@mdi/js"; import Icon from "@mdi/react"; import { Divider, @@ -8,8 +8,10 @@ import { ListItemText, Typography, } from "@mui/material"; +import React from "react"; import { useLocation } from "react-router-dom"; import { useAccounts } from "../hooks/AccountsListProvider"; +import { AccountWidget } from "./AccountWidget"; import { RouterLink } from "./RouterLink"; export function MoneyNavList(): React.ReactElement { @@ -59,7 +61,7 @@ export function MoneyNavList(): React.ReactElement { } + icon={} /> ))}