Compare commits
	
		
			50 Commits
		
	
	
		
			1.0.0
			...
			e6c332b1ed
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e6c332b1ed | |||
| dc3704f13b | |||
| af4c3c4a8f | |||
| a268e1bc4d | |||
| 77fc3699d6 | |||
| 96b3f6d4dc | |||
| 9a74c6c951 | |||
| c64fa838c9 | |||
| 8f45109e87 | |||
| d0c20aa68b | |||
| b84305428c | |||
| 560e415cb1 | |||
| 4863b1f4af | |||
| 6fa87a67d6 | |||
| e7219d2e44 | |||
| c9af4e4fc9 | |||
| 6cb4bcf4d4 | |||
| 447b6d89fb | |||
| a2a7d3d94a | |||
| 05a5da7527 | |||
| 43f62802dc | |||
| 60f6b94b41 | |||
| 5aaefafb48 | |||
| 63af408224 | |||
| a5966c8d53 | |||
| 196cb0b5c9 | |||
| ba06cd04ff | |||
| dc4f41f31c | |||
| 9ba928102c | |||
| 7d7ad0ce89 | |||
| d0ff90701f | |||
| f38482757e | |||
| 7e976d903d | |||
| 737d7f76d4 | |||
| cf0e42ff0e | |||
| d2fa9bf9ee | |||
| fb5a85311c | |||
| 288d334615 | |||
| 87f017fc42 | |||
| 43fb8dcda6 | |||
| a3b9c7cdb1 | |||
| c42b8b1bda | |||
| bdcbe94c97 | |||
| caeff985c2 | |||
| bed538793d | |||
| 602f20ad18 | |||
| 457c96b37e | |||
| a3f2b77548 | |||
| 3c5c82371a | |||
| fb46626cff | 
							
								
								
									
										10
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.drone.yml
									
									
									
									
									
								
							@@ -38,6 +38,7 @@ steps:
 | 
				
			|||||||
    - cd moneymgr_backend
 | 
					    - cd moneymgr_backend
 | 
				
			||||||
    - rustup component add clippy
 | 
					    - rustup component add clippy
 | 
				
			||||||
    - cargo clippy -- -D warnings
 | 
					    - cargo clippy -- -D warnings
 | 
				
			||||||
 | 
					    - cargo clippy --example api_curl -- -D warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: backend_test
 | 
					- name: backend_test
 | 
				
			||||||
  image: rust
 | 
					  image: rust
 | 
				
			||||||
@@ -51,7 +52,7 @@ steps:
 | 
				
			|||||||
    - cargo test
 | 
					    - cargo test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- name: backend_compile
 | 
					- name: backend_build
 | 
				
			||||||
  image: rust
 | 
					  image: rust
 | 
				
			||||||
  volumes:
 | 
					  volumes:
 | 
				
			||||||
  - name: rust_registry
 | 
					  - name: rust_registry
 | 
				
			||||||
@@ -67,15 +68,16 @@ steps:
 | 
				
			|||||||
  - cd moneymgr_backend
 | 
					  - cd moneymgr_backend
 | 
				
			||||||
  - mv /tmp/web_build/dist static
 | 
					  - mv /tmp/web_build/dist static
 | 
				
			||||||
  - cargo build --release
 | 
					  - cargo build --release
 | 
				
			||||||
  - ls -lah target/release/moneymgr_backend
 | 
					  - cargo build --release --example api_curl
 | 
				
			||||||
  - cp target/release/moneymgr_backend /tmp/release
 | 
					  - ls -lah target/release/moneymgr_backend target/release/examples/api_curl
 | 
				
			||||||
 | 
					  - cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Release
 | 
					# Release
 | 
				
			||||||
- name: gitea_release
 | 
					- name: gitea_release
 | 
				
			||||||
  image: plugins/gitea-release
 | 
					  image: plugins/gitea-release
 | 
				
			||||||
  depends_on:
 | 
					  depends_on:
 | 
				
			||||||
  - backend_compile
 | 
					  - backend_build
 | 
				
			||||||
  when:
 | 
					  when:
 | 
				
			||||||
    event:
 | 
					    event:
 | 
				
			||||||
      - tag
 | 
					      - tag
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
								
							@@ -3,10 +3,46 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Open Source web-based personal expenses tool.
 | 
					Open Source web-based personal expenses tool.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Note :** This project does not handle authentication itself. Instead, it relies on OpenID to achieve users authentication.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Setup prod env
 | 
				
			||||||
 | 
					1. Install prerequisites:
 | 
				
			||||||
 | 
					   1. docker
 | 
				
			||||||
 | 
					   2. docker compose
 | 
				
			||||||
 | 
					   3. git
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Clone this git repository:
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					git clone https://gitea.communiquons.org/pierre/MoneyMgr
 | 
				
			||||||
 | 
					cd MoneyMgr/docker_prod
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Copy and adapt env values
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					cp .env.sample .env
 | 
				
			||||||
 | 
					nano .env
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. Create required directories:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					mkdir -p storage/{db,redis-data,redis-conf,minio}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					5. Start containers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					docker compose up
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					6. Checkout http://localhost:8000/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> The default credentials are `admin` / `admin`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Setup dev env
 | 
					## Setup dev env
 | 
				
			||||||
1. Install prerequisites:
 | 
					1. Install prerequisites:
 | 
				
			||||||
   1. docker
 | 
					   1. docker
 | 
				
			||||||
   2. docker-compose
 | 
					   2. docker compose
 | 
				
			||||||
   3. rust
 | 
					   3. rust
 | 
				
			||||||
   4. node
 | 
					   4. node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								docker_prod/.env.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docker_prod/.env.sample
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					MINIO_ROOT_USER=rootuser
 | 
				
			||||||
 | 
					MINIO_ROOT_PASSWORD=rootpassword
 | 
				
			||||||
 | 
					DB_USER=db_user
 | 
				
			||||||
 | 
					DB_PASSWORD=db_password
 | 
				
			||||||
 | 
					REDIS_PASS=redis_password
 | 
				
			||||||
 | 
					WEBSITE_ORIGIN=http://localhost:8000
 | 
				
			||||||
 | 
					APP_SECRET=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
 | 
				
			||||||
 | 
					AUTH_SECRET_KEY=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
 | 
				
			||||||
 | 
					OIDC_CLIENT_ID=bar
 | 
				
			||||||
 | 
					OIDC_CLIENT_SECRET=foo
 | 
				
			||||||
							
								
								
									
										3
									
								
								docker_prod/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								docker_prod/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					.env
 | 
				
			||||||
 | 
					storage
 | 
				
			||||||
 | 
					auth/users.json
 | 
				
			||||||
							
								
								
									
										5
									
								
								docker_prod/auth/clients.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								docker_prod/auth/clients.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					- id: ${OIDC_CLIENT_ID}
 | 
				
			||||||
 | 
					  name: MoneyMgr
 | 
				
			||||||
 | 
					  description: Money management tool
 | 
				
			||||||
 | 
					  secret: ${OIDC_CLIENT_SECRET}
 | 
				
			||||||
 | 
					  redirect_uri: ${APP_ORIGIN}/oidc_cb
 | 
				
			||||||
							
								
								
									
										79
									
								
								docker_prod/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								docker_prod/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					services:
 | 
				
			||||||
 | 
					  minio:
 | 
				
			||||||
 | 
					    image: minio/minio
 | 
				
			||||||
 | 
					    user: "1000"
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - MINIO_ROOT_USER=$MINIO_ROOT_USER
 | 
				
			||||||
 | 
					      - MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./storage/minio:/data
 | 
				
			||||||
 | 
					    command: [ "minio", "server", "/data", "--console-address", ":9090" ]
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 9000:9000
 | 
				
			||||||
 | 
					      - 9090:9090
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - 9000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  db:
 | 
				
			||||||
 | 
					    image: postgres
 | 
				
			||||||
 | 
					    user: "1000"
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "5432:5432"
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - 5432
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - POSTGRES_USER=$DB_USER
 | 
				
			||||||
 | 
					      - POSTGRES_PASSWORD=$DB_PASSWORD
 | 
				
			||||||
 | 
					      - POSTGRES_DB=moneymgr
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./storage/db:/var/lib/postgresql/data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  oidc:
 | 
				
			||||||
 | 
					    image: pierre42100/basic_oidc
 | 
				
			||||||
 | 
					    user: "1000"
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - LISTEN_ADDRESS=0.0.0.0:9001
 | 
				
			||||||
 | 
					      - STORAGE_PATH=/storage
 | 
				
			||||||
 | 
					      - TOKEN_KEY=$AUTH_SECRET_KEY
 | 
				
			||||||
 | 
					      - WEBSITE_ORIGIN=http://localhost:9001
 | 
				
			||||||
 | 
					      - OIDC_CLIENT_ID=$OIDC_CLIENT_ID
 | 
				
			||||||
 | 
					      - OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
 | 
				
			||||||
 | 
					      - APP_ORIGIN=$WEBSITE_ORIGIN
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - 9001
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 9001:9001
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./auth:/storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  redis:
 | 
				
			||||||
 | 
					    image: redis:alpine
 | 
				
			||||||
 | 
					    user: "1000"
 | 
				
			||||||
 | 
					    command: redis-server --requirepass ${REDIS_PASS:-secretredis}
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - 6379
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ./storage/redis-data:/data
 | 
				
			||||||
 | 
					      - ./storage/redis-conf:/usr/local/etc/redis/redis.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  moneymgr:
 | 
				
			||||||
 | 
					    image: pierre42100/moneymgr_backend
 | 
				
			||||||
 | 
					    user: "1000"
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 8000:8000
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - WEBSITE_ORIGIN=${WEBSITE_ORIGIN}
 | 
				
			||||||
 | 
					      - SECRET=${APP_SECRET}
 | 
				
			||||||
 | 
					      - DB_HOST=db
 | 
				
			||||||
 | 
					      - DB_USERNAME=$DB_USER
 | 
				
			||||||
 | 
					      - DB_PASSWORD=$DB_PASSWORD
 | 
				
			||||||
 | 
					      - DB_NAME=moneymgr
 | 
				
			||||||
 | 
					      - OIDC_CONFIGURATION_URL=http://oidc:9001/.well-known/openid-configuration
 | 
				
			||||||
 | 
					      - OIDC_PROVIDER_NAME=OIDC
 | 
				
			||||||
 | 
					      - OIDC_CLIENT_ID=$OIDC_CLIENT_ID
 | 
				
			||||||
 | 
					      - OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
 | 
				
			||||||
 | 
					      - S3_ENDPOINT=http://minio:9000
 | 
				
			||||||
 | 
					      - S3_ACCESS_KEY=$MINIO_ROOT_USER
 | 
				
			||||||
 | 
					      - S3_SECRET_KEY=$MINIO_ROOT_PASSWORD
 | 
				
			||||||
 | 
					      - REDIS_HOSTNAME=redis
 | 
				
			||||||
 | 
					      - REDIS_PASSWORD=${REDIS_PASS:-secretredis}
 | 
				
			||||||
							
								
								
									
										665
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										665
									
								
								moneymgr_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -6,33 +6,34 @@ edition = "2024"
 | 
				
			|||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
env_logger = "0.11.8"
 | 
					env_logger = "0.11.8"
 | 
				
			||||||
log = "0.4.27"
 | 
					log = "0.4.27"
 | 
				
			||||||
diesel = { version = "2.2.0", features = ["postgres", "r2d2"] }
 | 
					diesel = { version = "2.2.11", features = ["postgres", "r2d2"] }
 | 
				
			||||||
diesel_migrations = "2.1.0"
 | 
					diesel_migrations = "2.2.0"
 | 
				
			||||||
clap = { version = "4.5.35", features = ["env", "derive"] }
 | 
					clap = { version = "4.5.40", features = ["env", "derive"] }
 | 
				
			||||||
actix-web = "4"
 | 
					actix-web = "4.11.0"
 | 
				
			||||||
actix-cors = "0.7.0"
 | 
					actix-cors = "0.7.1"
 | 
				
			||||||
actix-multipart = "0.7.0"
 | 
					actix-multipart = "0.7.2"
 | 
				
			||||||
actix-remote-ip = "0.1.0"
 | 
					actix-remote-ip = "0.1.0"
 | 
				
			||||||
actix-session = { version = "0.10.0", features = ["redis-session"] }
 | 
					actix-session = { version = "0.10.1", features = ["redis-session"] }
 | 
				
			||||||
actix-files = "0.6.6"
 | 
					actix-files = "0.6.6"
 | 
				
			||||||
lazy_static = "1.5.0"
 | 
					lazy_static = "1.5.0"
 | 
				
			||||||
anyhow = "1.0.97"
 | 
					anyhow = "1.0.98"
 | 
				
			||||||
serde = { version = "1.0.219", features = ["derive"] }
 | 
					serde = { version = "1.0.219", features = ["derive"] }
 | 
				
			||||||
rust-s3 = "0.36.0-beta.2"
 | 
					rust-s3 = "0.36.0-beta.2"
 | 
				
			||||||
thiserror = "2.0.12"
 | 
					thiserror = "2.0.12"
 | 
				
			||||||
tokio = "1.44.1"
 | 
					tokio = "1.45.1"
 | 
				
			||||||
futures-util = "0.3.31"
 | 
					futures-util = "0.3.31"
 | 
				
			||||||
serde_json = "1.0.140"
 | 
					serde_json = "1.0.140"
 | 
				
			||||||
light-openid = "1.0.4"
 | 
					light-openid = "1.0.4"
 | 
				
			||||||
rand = "0.9.0"
 | 
					rand = "0.9.1"
 | 
				
			||||||
ipnet = { version = "2.11.0", features = ["serde"] }
 | 
					ipnet = { version = "2.11.0", features = ["serde"] }
 | 
				
			||||||
lazy-regex = "3.4.1"
 | 
					lazy-regex = "3.4.1"
 | 
				
			||||||
jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] }
 | 
					jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] }
 | 
				
			||||||
mime_guess = "2.0.5"
 | 
					mime_guess = "2.0.5"
 | 
				
			||||||
rust-embed = { version = "8.6.0" }
 | 
					rust-embed = { version = "8.7.2" }
 | 
				
			||||||
sha2 = "0.10.8"
 | 
					sha2 = "0.11.0-rc.0"
 | 
				
			||||||
 | 
					base16ct = "0.2.0"
 | 
				
			||||||
httpdate = "1.0.3"
 | 
					httpdate = "1.0.3"
 | 
				
			||||||
chrono = "0.4.41"
 | 
					chrono = "0.4.41"
 | 
				
			||||||
tempfile = "3.19.1"
 | 
					tempfile = "3.20.0"
 | 
				
			||||||
zip = "2.6.1"
 | 
					zip = "3.0.0"
 | 
				
			||||||
rust_xlsxwriter = "0.86.1"
 | 
					rust_xlsxwriter = "0.87.0"
 | 
				
			||||||
@@ -14,7 +14,7 @@ use std::process::Command;
 | 
				
			|||||||
struct Args {
 | 
					struct Args {
 | 
				
			||||||
    /// URL to Money manager API
 | 
					    /// URL to Money manager API
 | 
				
			||||||
    #[arg(short('U'), long, env, default_value = "http://localhost:8000/api")]
 | 
					    #[arg(short('U'), long, env, default_value = "http://localhost:8000/api")]
 | 
				
			||||||
    matrix_gw_url: String,
 | 
					    moneymgr_url: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Token ID
 | 
					    /// Token ID
 | 
				
			||||||
    #[arg(short('i'), long, env)]
 | 
					    #[arg(short('i'), long, env)]
 | 
				
			||||||
@@ -39,7 +39,8 @@ struct Args {
 | 
				
			|||||||
fn main() {
 | 
					fn main() {
 | 
				
			||||||
    let args: Args = Args::parse();
 | 
					    let args: Args = Args::parse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let full_url = format!("{}{}", args.matrix_gw_url, args.uri);
 | 
					    let full_url = format!("{}{}", args.moneymgr_url, args.uri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log::debug!("Full URL: {full_url}");
 | 
					    log::debug!("Full URL: {full_url}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key = HS256Key::from_bytes(args.token_secret.as_bytes());
 | 
					    let key = HS256Key::from_bytes(args.token_secret.as_bytes());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,12 +78,15 @@ pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) -
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
 | 
					/// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
 | 
				
			||||||
pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult {
 | 
					pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult {
 | 
				
			||||||
    let accounts = accounts_service::get_list_user(auth.user_id()).await?;
 | 
					    let mut accounts = accounts_service::get_list_user(auth.user_id()).await?;
 | 
				
			||||||
 | 
					    accounts.sort_by_key(|a| a.id());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut out = FinancesManagerFile { accounts: vec![] };
 | 
					    let mut out = FinancesManagerFile { accounts: vec![] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for account in accounts {
 | 
					    for account in accounts {
 | 
				
			||||||
        let movements = movements_service::get_list_account(account.id()).await?;
 | 
					        let mut movements = movements_service::get_list_account(account.id()).await?;
 | 
				
			||||||
 | 
					        movements.sort_by(|a, b| b.time.cmp(&a.time));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut file_account = FinancesManagerAccount {
 | 
					        let mut file_account = FinancesManagerAccount {
 | 
				
			||||||
            name: account.name,
 | 
					            name: account.name,
 | 
				
			||||||
            movements: Vec::with_capacity(movements.len()),
 | 
					            movements: Vec::with_capacity(movements.len()),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,5 +5,5 @@ pub fn sha512(bytes: &[u8]) -> String {
 | 
				
			|||||||
    let mut hasher = Sha512::new();
 | 
					    let mut hasher = Sha512::new();
 | 
				
			||||||
    hasher.update(bytes);
 | 
					    hasher.update(bytes);
 | 
				
			||||||
    let h = hasher.finalize();
 | 
					    let h = hasher.finalize();
 | 
				
			||||||
    format!("{:x}", h)
 | 
					    base16ct::lower::encode_string(h.as_slice())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1907
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1907
									
								
								moneymgr_web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -12,38 +12,38 @@
 | 
				
			|||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@emotion/react": "^11.14.0",
 | 
					    "@emotion/react": "^11.14.0",
 | 
				
			||||||
    "@emotion/styled": "^11.14.0",
 | 
					    "@emotion/styled": "^11.14.0",
 | 
				
			||||||
    "@fontsource/roboto": "^5.2.5",
 | 
					    "@fontsource/roboto": "^5.2.6",
 | 
				
			||||||
    "@jsonjoy.com/base64": "^1.1.2",
 | 
					    "@jsonjoy.com/base64": "^1.1.2",
 | 
				
			||||||
    "@mdi/js": "^7.4.47",
 | 
					    "@mdi/js": "^7.4.47",
 | 
				
			||||||
    "@mdi/react": "^1.6.1",
 | 
					    "@mdi/react": "^1.6.1",
 | 
				
			||||||
    "@mui/icons-material": "^7.0.1",
 | 
					    "@mui/icons-material": "^7.1.2",
 | 
				
			||||||
    "@mui/material": "^7.0.1",
 | 
					    "@mui/material": "^7.1.2",
 | 
				
			||||||
    "@mui/x-charts": "^8.2.0",
 | 
					    "@mui/x-charts": "^8.5.3",
 | 
				
			||||||
    "@mui/x-data-grid": "^7.28.3",
 | 
					    "@mui/x-data-grid": "^8.5.3",
 | 
				
			||||||
    "@mui/x-date-pickers": "^8.0.0-beta.3",
 | 
					    "@mui/x-date-pickers": "^8.5.3",
 | 
				
			||||||
    "date-and-time": "^3.6.0",
 | 
					    "date-and-time": "^3.6.0",
 | 
				
			||||||
    "dayjs": "^1.11.13",
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
    "filesize": "^10.1.6",
 | 
					    "filesize": "^10.1.6",
 | 
				
			||||||
    "qrcode.react": "^4.2.0",
 | 
					    "qrcode.react": "^4.2.0",
 | 
				
			||||||
    "react": "^19.1.0",
 | 
					    "react": "^19.1.0",
 | 
				
			||||||
    "react-dom": "^19.1.0",
 | 
					    "react-dom": "^19.1.0",
 | 
				
			||||||
    "react-router": "^7.4.1",
 | 
					    "react-router": "^7.6.2",
 | 
				
			||||||
    "react-router-dom": "^7.4.1",
 | 
					    "react-router-dom": "^7.6.2",
 | 
				
			||||||
    "ts-pattern": "^5.7.0"
 | 
					    "ts-pattern": "^5.7.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@eslint/js": "^9.23.0",
 | 
					    "@eslint/js": "^9.29.0",
 | 
				
			||||||
    "@types/react": "^19.1.0",
 | 
					    "@types/react": "^19.1.8",
 | 
				
			||||||
    "@types/react-dom": "^19.1.1",
 | 
					    "@types/react-dom": "^19.1.6",
 | 
				
			||||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
					    "@vitejs/plugin-react": "^4.6.0",
 | 
				
			||||||
    "eslint": "^9.23.0",
 | 
					    "eslint": "^9.30.1",
 | 
				
			||||||
    "eslint-plugin-react-dom": "^1.40.2",
 | 
					    "eslint-plugin-react-dom": "^1.49.0",
 | 
				
			||||||
    "eslint-plugin-react-hooks": "^5.1.0",
 | 
					    "eslint-plugin-react-hooks": "^5.2.0",
 | 
				
			||||||
    "eslint-plugin-react-refresh": "^0.4.19",
 | 
					    "eslint-plugin-react-refresh": "^00.4.20",
 | 
				
			||||||
    "eslint-plugin-react-x": "^1.40.2",
 | 
					    "eslint-plugin-react-x": "^1.52.2",
 | 
				
			||||||
    "globals": "^16.0.0",
 | 
					    "globals": "^16.2.0",
 | 
				
			||||||
    "typescript": "~5.8.2",
 | 
					    "typescript": "~5.8.3",
 | 
				
			||||||
    "typescript-eslint": "^8.29.0",
 | 
					    "typescript-eslint": "^8.32.1",
 | 
				
			||||||
    "vite": "^6.2.5"
 | 
					    "vite": "^6.3.5"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -121,16 +121,18 @@ function MovementsTable(p: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const chooseAccount = useSelectAccount();
 | 
					  const chooseAccount = useSelectAccount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [labelFilter, setLabelFilter] = React.useState("");
 | 
					  const [filter, setFilter] = React.useState("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const filteredList = React.useMemo(() => {
 | 
					  const filteredList = React.useMemo(() => {
 | 
				
			||||||
    return p.movements.filter((m) =>
 | 
					    return p.movements.filter(
 | 
				
			||||||
      m.label.toLowerCase().includes(labelFilter.toLowerCase())
 | 
					      (m) =>
 | 
				
			||||||
 | 
					        m.label.toLowerCase().includes(filter.toLowerCase()) ||
 | 
				
			||||||
 | 
					        m.amount.toString().includes(filter)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }, [p.movements, labelFilter]);
 | 
					  }, [p.movements, filter]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [rowSelectionModel, setRowSelectionModel] =
 | 
					  const [rowSelectionModel, setRowSelectionModel] =
 | 
				
			||||||
    React.useState<GridRowSelectionModel>([]);
 | 
					    React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Set uploaded file
 | 
					  // Set uploaded file
 | 
				
			||||||
  const setUploadedFile = async (
 | 
					  const setUploadedFile = async (
 | 
				
			||||||
@@ -216,7 +218,7 @@ function MovementsTable(p: {
 | 
				
			|||||||
  const moveMultiple = async () => {
 | 
					  const moveMultiple = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const movements = p.movements.filter((m) =>
 | 
					      const movements = p.movements.filter((m) =>
 | 
				
			||||||
        rowSelectionModel.includes(m.id)
 | 
					        rowSelectionModel.ids.has(m.id)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const targetAccount = await chooseAccount(
 | 
					      const targetAccount = await chooseAccount(
 | 
				
			||||||
@@ -260,7 +262,7 @@ function MovementsTable(p: {
 | 
				
			|||||||
  const deleteMultiple = async () => {
 | 
					  const deleteMultiple = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const movements = p.movements.filter((m) =>
 | 
					      const movements = p.movements.filter((m) =>
 | 
				
			||||||
        rowSelectionModel.includes(m.id)
 | 
					        rowSelectionModel.ids.has(m.id)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
@@ -382,26 +384,30 @@ function MovementsTable(p: {
 | 
				
			|||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <div style={{ display: "flex" }}>
 | 
					      <div style={{ display: "flex" }}>
 | 
				
			||||||
        <TextField
 | 
					        <TextField
 | 
				
			||||||
          placeholder="Filter by label"
 | 
					          placeholder="Filter by label or amount"
 | 
				
			||||||
          variant="standard"
 | 
					          variant="standard"
 | 
				
			||||||
          size="small"
 | 
					          size="small"
 | 
				
			||||||
          value={labelFilter}
 | 
					          value={filter}
 | 
				
			||||||
          onChange={(e) => {
 | 
					          onChange={(e) => {
 | 
				
			||||||
            setLabelFilter(e.target.value);
 | 
					            setFilter(e.target.value);
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          style={{ padding: "0px", flex: 1 }}
 | 
					          style={{ padding: "0px", flex: 1 }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <span style={{ flex: 1 }}></span>
 | 
					        <span style={{ flex: 1 }}></span>
 | 
				
			||||||
        <Tooltip title="Refresh table">
 | 
					        <Tooltip title="Refresh table">
 | 
				
			||||||
          <IconButton onClick={() => { p.needReload(false); }}>
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              p.needReload(false);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <RefreshIcon />
 | 
					            <RefreshIcon />
 | 
				
			||||||
          </IconButton>
 | 
					          </IconButton>
 | 
				
			||||||
        </Tooltip>
 | 
					        </Tooltip>
 | 
				
			||||||
        <Tooltip title="Move all the selected entries to another account">
 | 
					        <Tooltip title="Move all the selected entries to another account">
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            disabled={
 | 
					            disabled={
 | 
				
			||||||
              rowSelectionModel.length === 0 ||
 | 
					              rowSelectionModel.ids.size === 0 ||
 | 
				
			||||||
              rowSelectionModel.length === p.movements.length
 | 
					              rowSelectionModel.ids.size === p.movements.length
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            onClick={moveMultiple}
 | 
					            onClick={moveMultiple}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
@@ -411,8 +417,8 @@ function MovementsTable(p: {
 | 
				
			|||||||
        <Tooltip title="Delete all the selected entries">
 | 
					        <Tooltip title="Delete all the selected entries">
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            disabled={
 | 
					            disabled={
 | 
				
			||||||
              rowSelectionModel.length === 0 ||
 | 
					              rowSelectionModel.ids.size === 0 ||
 | 
				
			||||||
              rowSelectionModel.length === p.movements.length
 | 
					              rowSelectionModel.ids.size === p.movements.length
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            onClick={deleteMultiple}
 | 
					            onClick={deleteMultiple}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,7 +158,7 @@ function ImportExportModal(p: {
 | 
				
			|||||||
        </CardContent>{" "}
 | 
					        </CardContent>{" "}
 | 
				
			||||||
        <CardActions>
 | 
					        <CardActions>
 | 
				
			||||||
          <span style={{ flex: 1 }}>
 | 
					          <span style={{ flex: 1 }}>
 | 
				
			||||||
            <RouterLink to={p.exportURL}>
 | 
					            <RouterLink to={p.exportURL} target="_blank">
 | 
				
			||||||
              <Button
 | 
					              <Button
 | 
				
			||||||
                startIcon={<DownloadIcon />}
 | 
					                startIcon={<DownloadIcon />}
 | 
				
			||||||
                variant="outlined"
 | 
					                variant="outlined"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -133,7 +133,7 @@ function InboxTable(p: {
 | 
				
			|||||||
  }, [p.entries, labelFilter]);
 | 
					  }, [p.entries, labelFilter]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [rowSelectionModel, setRowSelectionModel] =
 | 
					  const [rowSelectionModel, setRowSelectionModel] =
 | 
				
			||||||
    React.useState<GridRowSelectionModel>([]);
 | 
					    React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [attaching, setAttaching] = React.useState<InboxEntry | undefined>();
 | 
					  const [attaching, setAttaching] = React.useState<InboxEntry | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -244,7 +244,7 @@ function InboxTable(p: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // Find the entry to map
 | 
					      // Find the entry to map
 | 
				
			||||||
      const entries = p.entries.filter(
 | 
					      const entries = p.entries.filter(
 | 
				
			||||||
        (m) => rowSelectionModel.includes(m.id) && !m.movement_id
 | 
					        (m) => rowSelectionModel.ids.has(m.id) && !m.movement_id
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      const movements: Movement[][] = [];
 | 
					      const movements: Movement[][] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -324,7 +324,7 @@ function InboxTable(p: {
 | 
				
			|||||||
  const deleteMultiple = async () => {
 | 
					  const deleteMultiple = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const deletedEntries = p.entries.filter((m) =>
 | 
					      const deletedEntries = p.entries.filter((m) =>
 | 
				
			||||||
        rowSelectionModel.includes(m.id)
 | 
					        rowSelectionModel.ids.has(m.id)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
@@ -437,7 +437,9 @@ function InboxTable(p: {
 | 
				
			|||||||
              icon={<SearchIcon />}
 | 
					              icon={<SearchIcon />}
 | 
				
			||||||
              label="Attach entry to movement"
 | 
					              label="Attach entry to movement"
 | 
				
			||||||
              color="inherit"
 | 
					              color="inherit"
 | 
				
			||||||
              onClick={() => { handleAttachClick(params.row); }}
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                handleAttachClick(params.row);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
              disabled={!!params.row.movement_id}
 | 
					              disabled={!!params.row.movement_id}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </Tooltip>,
 | 
					          </Tooltip>,
 | 
				
			||||||
@@ -496,7 +498,7 @@ function InboxTable(p: {
 | 
				
			|||||||
        </Tooltip>
 | 
					        </Tooltip>
 | 
				
			||||||
        <Tooltip title="Attach all the selected inbox entries to movements">
 | 
					        <Tooltip title="Attach all the selected inbox entries to movements">
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            disabled={rowSelectionModel.length === 0}
 | 
					            disabled={rowSelectionModel.ids.size === 0}
 | 
				
			||||||
            onClick={attachMultiple}
 | 
					            onClick={attachMultiple}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <SearchIcon />
 | 
					            <SearchIcon />
 | 
				
			||||||
@@ -505,8 +507,8 @@ function InboxTable(p: {
 | 
				
			|||||||
        <Tooltip title="Delete all the selected inbox entries">
 | 
					        <Tooltip title="Delete all the selected inbox entries">
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            disabled={
 | 
					            disabled={
 | 
				
			||||||
              rowSelectionModel.length === 0 ||
 | 
					              rowSelectionModel.ids.size === 0 ||
 | 
				
			||||||
              rowSelectionModel.length === p.entries.length
 | 
					              rowSelectionModel.ids.size === p.entries.length
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            onClick={deleteMultiple}
 | 
					            onClick={deleteMultiple}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { DatePicker } from "@mui/x-date-pickers";
 | 
					import { DateField } from "@mui/x-date-pickers";
 | 
				
			||||||
import { dateToTime, timeToDate } from "../../utils/DateUtils";
 | 
					import { dateToTime, timeToDate } from "../../utils/DateUtils";
 | 
				
			||||||
import { TextFieldVariants } from "@mui/material";
 | 
					import { TextFieldVariants } from "@mui/material";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,13 +13,16 @@ export function DateInput(p: {
 | 
				
			|||||||
  variant?: TextFieldVariants;
 | 
					  variant?: TextFieldVariants;
 | 
				
			||||||
}): React.ReactElement {
 | 
					}): React.ReactElement {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <DatePicker
 | 
					    <DateField
 | 
				
			||||||
      autoFocus={p.autoFocus}
 | 
					      autoFocus={p.autoFocus}
 | 
				
			||||||
      readOnly={p.editable === false}
 | 
					      readOnly={p.editable === false}
 | 
				
			||||||
      label={p.label}
 | 
					      label={p.label}
 | 
				
			||||||
      slotProps={{
 | 
					      slotProps={{
 | 
				
			||||||
        field: { ref: p.ref },
 | 
					        textField: {
 | 
				
			||||||
        textField: { variant: p.variant ?? "standard", style: p.style },
 | 
					          ref: p.ref,
 | 
				
			||||||
 | 
					          variant: p.variant ?? "standard",
 | 
				
			||||||
 | 
					          style: p.style,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      value={timeToDate(p.value)}
 | 
					      value={timeToDate(p.value)}
 | 
				
			||||||
      onChange={(v) => {
 | 
					      onChange={(v) => {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user