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={}
/>
))}