79 Commits

Author SHA1 Message Date
23cc189e53 Fix date extraction
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-20 19:17:47 +02:00
3098d12e8a Support short dates
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-20 18:32:02 +02:00
0943104cc8 Can show expense in full screen
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 18:23:37 +02:00
3beaba806a Can clear cost value quickly 2025-07-20 18:18:20 +02:00
1788e7f184 Can disable dates extraction 2025-07-20 18:14:03 +02:00
71d32d72ef Can extract date of expenses
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 18:07:22 +02:00
28f61a3099 Improve regular expression
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 17:44:46 +02:00
f61e3541fb Perform first OCR extraction 2025-07-20 17:25:52 +02:00
fb7891d913 Merge pull request 'Update Rust crate serde_json to 1.0.141' (#63) from renovate/serde_json-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-20 00:15:22 +00:00
d9ede224cf Update Rust crate serde_json to 1.0.141
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-20 00:15:16 +00:00
fc9334b20b Merge pull request 'Update dependency dart to ^3.8.2' (#62) from renovate/dart-3.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-19 00:18:13 +00:00
c4cbd7ec8b Update dependency dart to ^3.8.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-19 00:17:10 +00:00
a4ef3e74dc Optimize APK size
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-18 18:37:58 +02:00
dbb988f2b5 Add mobile application (#47)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #47
2025-07-18 16:06:21 +00:00
b2aff4902d Fix Flutter code quality issues
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-18 18:01:25 +02:00
6f578b39f9 Updated project dependencies
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-18 17:57:57 +02:00
de519ecb6c Fix generated APK name
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2025-07-18 11:25:19 +02:00
3049e545e9 Fix keystore script file
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-18 10:08:51 +02:00
1f1c01a287 Sign APK builds from CI
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-18 09:04:01 +02:00
92885b8af6 Merge pull request 'Update dependency globals to ^16.3.0' (#61) from renovate/globals-16.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-18 00:51:18 +00:00
44320db760 Update dependency globals to ^16.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-18 00:16:24 +00:00
1f2a28aa65 Fix signing configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 22:58:59 +02:00
f9566315eb Create a publish flavor dedicated for public releases
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 22:50:12 +02:00
63bed07015 Change debug configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 20:36:44 +02:00
4b84d926d4 Disallow app backup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 20:32:13 +02:00
8191a28986 Fix Drone configuration
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 20:27:24 +02:00
8c30b50d0c Update
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 20:25:31 +02:00
389b2c96ba Remove temporary comments in Drone configuration
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-17 19:51:01 +02:00
5a08b0c971 Test full APK build before release
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-17 19:31:39 +02:00
b3fd066633 Fix clone
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2025-07-17 19:26:18 +02:00
5c987473a5 Unshallow Git clone
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-17 19:25:27 +02:00
c3d2612f9a Second test
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 19:21:49 +02:00
130cc1ef0d Temp test
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 19:20:44 +02:00
aebefd114a Reorganize mobile app building 2025-07-17 19:19:20 +02:00
34d3e08149 Improve pipeline
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-17 19:08:21 +02:00
ccd3540804 Remove mobile app dependencies
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 18:53:12 +02:00
b9b871224b Add flutter build to CI
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 18:52:16 +02:00
17a22d7a4c Add an option to start on scans screen instead of capture screen
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 18:04:03 +02:00
8db2cf3ece Show a message when expenses list is empty
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 17:51:13 +02:00
e45648e038 Fix: reset editor screen between captures 2025-07-17 17:49:01 +02:00
55144da943 Can upload expenses to server 2025-07-17 17:44:36 +02:00
5065f780f2 Merge pull request 'Update dependency @mui/x-date-pickers to ^8.8.0' (#60) from renovate/mui-x-date-pickers-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-17 00:46:19 +00:00
28d8b96ebe Update dependency @mui/x-date-pickers to ^8.8.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-17 00:16:12 +00:00
baf62aa2a5 Fix misleading comment
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-16 11:30:25 +00:00
8a9a8d6b14 Merge remote-tracking branch 'origin/main' into mobile-app
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-16 11:06:52 +02:00
c19d46a50f Merge pull request 'Update dependency @mui/x-data-grid to ^8.8.0' (#59) from renovate/mui-x-data-grid-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-16 00:39:44 +00:00
f001c618cd Update dependency @mui/x-data-grid to ^8.8.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-16 00:15:46 +00:00
f9d46e46a5 Fix ESLint issue
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-15 21:34:19 +02:00
96f1bf589c Can clear search filter in account page
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-15 21:33:19 +02:00
8ec6e48938 Start to build synchronization logic
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-15 21:14:09 +02:00
235fda5c72 Can delete expense 2025-07-15 20:11:43 +02:00
2568ea14b4 Can update scanned expenses entries 2025-07-15 20:00:55 +02:00
467393dad0 Merge pull request 'Update dependency @mui/x-charts to ^8.8.0' (#58) from renovate/mui-x-charts-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-15 00:36:12 +00:00
f619f26e93 Update dependency @mui/x-charts to ^8.8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-15 00:15:36 +00:00
cecb7a0cd1 Remove temporary fix on label style
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 16:50:53 +02:00
50812af2fc Display the list of expenses 2025-07-14 16:49:32 +02:00
547e9b7aad Can save expenses to local list
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 16:24:59 +02:00
dd035f8a15 Fix PDF rendering on my smartphone
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 16:03:45 +02:00
cc4ce19af2 Merge pull request 'Update dependency @eslint/js to ^9.31.0' (#57) from renovate/eslint-js-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-14 00:38:25 +00:00
192dc5827b Update dependency @eslint/js to ^9.31.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 00:15:23 +00:00
37674a6229 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.3' (#56) from renovate/eslint-plugin-react-x-1.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-13 00:40:31 +00:00
ef86667029 Update dependency eslint-plugin-react-x to ^1.52.3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-13 00:15:07 +00:00
07f63a96fa Merge pull request 'Update Rust crate diesel to 2.2.12' (#55) from renovate/diesel-2.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-12 00:47:30 +00:00
fa88a3c9ed Update Rust crate diesel to 2.2.12
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-12 00:15:34 +00:00
85c6a0b955 Merge pull request 'Update dependency @mui/x-date-pickers to ^8.7.0' (#54) from renovate/mui-x-date-pickers-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-11 00:41:02 +00:00
21ee97b8a4 Update dependency @mui/x-date-pickers to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-11 00:15:43 +00:00
119f026a21 Merge pull request 'Update Rust crate clap to 4.5.41' (#53) from renovate/clap-4.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-10 00:56:57 +00:00
d72acfac9b Update Rust crate clap to 4.5.41
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-10 00:16:25 +00:00
77c8866bb8 Merge pull request 'Update dependency @mui/x-data-grid to ^8.7.0' (#52) from renovate/mui-x-data-grid-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-09 00:44:36 +00:00
133f235639 Update dependency @mui/x-data-grid to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-09 00:15:58 +00:00
7ef0499abf Merge pull request 'Update dependency @mui/x-charts to ^8.7.0' (#51) from renovate/mui-x-charts-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-08 00:45:34 +00:00
1383da4483 Update dependency @mui/x-charts to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-08 00:16:00 +00:00
4f1a9d0865 Merge pull request 'Update dependency @eslint/js to ^9.30.1' (#50) from renovate/eslint-js-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-07 00:40:39 +00:00
31803feaa9 Update dependency @eslint/js to ^9.30.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-07 00:15:37 +00:00
aad32f9c25 Merge pull request 'Update dependency react-router-dom to ^7.6.3' (#49) from renovate/react-router-dom-7.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-06 00:42:40 +00:00
1ea2bd6acf Update dependency react-router-dom to ^7.6.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-06 00:15:52 +00:00
a085116018 Merge pull request 'Update dependency react-router to ^7.6.3' (#48) from renovate/react-router-7.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-05 00:50:00 +00:00
952a66042c Update dependency react-router to ^7.6.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-05 00:09:30 +00:00
6cf6ab5a37 Update dependency @emotion/styled to ^11.14.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-04 00:09:24 +00:00
34 changed files with 1252 additions and 300 deletions

View File

@@ -4,9 +4,17 @@ type: docker
name: default
steps:
# Needs a full git clone
- name: fetch
image: alpine/git
commands:
- git fetch --tags
# Frontend
- name: web_build
image: node:23
depends_on:
- fetch
volumes:
- name: web_app
path: /tmp/web_build
@@ -20,6 +28,8 @@ steps:
# Backend
- name: backend_fetch_deps
image: rust
depends_on:
- fetch
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
@@ -54,6 +64,9 @@ steps:
- name: backend_build
image: rust
when:
event:
- tag
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
@@ -72,12 +85,58 @@ steps:
- ls -lah target/release/moneymgr_backend target/release/examples/api_curl
- cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release
# Mobile app code quality
- name: mobile_app_code_quality
image: ghcr.io/cirruslabs/flutter:latest
depends_on:
- fetch
commands:
- echo "Build version:" $(git describe --tags --abbrev=0)
- echo "Build number:" $(git rev-list --count $(git describe --tags --abbrev=0))
- cd moneymgr_mobile
- flutter --disable-analytics
- flutter pub get --enforce-lockfile
- dart run build_runner build
- flutter analyze
# Mobile app build
- name: mobile_app_build
image: ghcr.io/cirruslabs/flutter:latest
depends_on:
- backend_build # prevent synchronous backend & frontend build
- mobile_app_code_quality
when:
event:
- tag
environment:
JKS_KEYSTORE:
from_secret: JKS_KEYSTORE
JKS_KEYSTORE_PASSWORD:
from_secret: JKS_KEYSTORE_PASSWORD
volumes:
- name: release
path: /tmp/release
commands:
- cd moneymgr_mobile
- flutter --disable-analytics
- bash android/ci_write_keystore.sh
- flutter pub get --enforce-lockfile
- dart run build_runner build
- flutter build apk
--release
--flavor publish
--build-name $(git describe --tags --abbrev=0)
--split-per-abi
--target-platform android-arm64
--build-number $(git rev-list --count $(git describe --tags --abbrev=0))
- cp build/app/outputs/flutter-apk/app-arm64-v8a-publish-release.apk /tmp/release/moneymgr_mobile_arm64-v8a.apk
# Release
- name: gitea_release
image: plugins/gitea-release
depends_on:
- backend_build
- mobile_app_build
when:
event:
- tag

View File

@@ -724,9 +724,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.40"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
dependencies = [
"clap_builder",
"clap_derive",
@@ -734,9 +734,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.40"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
dependencies = [
"anstream",
"anstyle",
@@ -746,9 +746,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.40"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
dependencies = [
"heck",
"proc-macro2",
@@ -1100,9 +1100,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "2.2.11"
version = "2.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614"
checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c"
dependencies = [
"bitflags",
"byteorder",
@@ -3232,9 +3232,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [
"itoa",
"memchr",

View File

@@ -6,9 +6,9 @@ edition = "2024"
[dependencies]
env_logger = "0.11.8"
log = "0.4.27"
diesel = { version = "2.2.11", features = ["postgres", "r2d2"] }
diesel = { version = "2.2.12", features = ["postgres", "r2d2"] }
diesel_migrations = "2.2.0"
clap = { version = "4.5.40", features = ["env", "derive"] }
clap = { version = "4.5.41", features = ["env", "derive"] }
actix-web = "4.11.0"
actix-cors = "0.7.1"
actix-multipart = "0.7.2"
@@ -22,7 +22,7 @@ rust-s3 = "0.36.0-beta.2"
thiserror = "2.0.12"
tokio = "1.45.1"
futures-util = "0.3.31"
serde_json = "1.0.140"
serde_json = "1.0.141"
light-openid = "1.0.4"
rand = "0.9.1"
ipnet = { version = "2.11.0", features = ["serde"] }

View File

@@ -56,7 +56,7 @@ impl FromRequest for AuthExtractor {
};
Box::pin(async move {
// Check for authentication using OpenID
// Check for authentication using API token
if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) {
let Ok(jwt_token) = token.to_str() else {
return Err(actix_web::error::ErrorBadRequest(

View File

@@ -1,3 +1,5 @@
publish_key.properties
gradle-wrapper.jar
/.gradle
/captures/

View File

@@ -0,0 +1,8 @@
# Android version of application
Generate keystore:
```bash
keytool -genkey -v -keystore ./keystore.jks -keyalg RSA \
-keysize 2048 -validity 20000 -alias moneymgr
```

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +8,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("publish_key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "org.communiquons.moneymgr"
compileSdk = flutter.compileSdkVersion
@@ -21,7 +30,6 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "org.communiquons.moneymgr"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
@@ -31,13 +39,34 @@ android {
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfigs {
create("publish") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
// signingConfig = signingConfigs.getByName("debug")
}
}
flavorDimensions += "default"
productFlavors {
create("development") {
dimension = "default"
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
create("publish") {
dimension = "default"
signingConfig = signingConfigs.getByName("publish")
}
}
}
flutter {

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
@@ -6,5 +7,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- In debug mode, unsecure traffic is permitted -->
<application android:usesCleartextTraffic="true" />
<application
android:label="MoneyMgr Debug"
android:usesCleartextTraffic="true"
tools:replace="android:label" />
</manifest>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false" />
</manifest>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</device-transfer>
</data-extraction-rules>

View File

@@ -0,0 +1,23 @@
#!/bin/bash
SCRIPT_DIR="$(temp=$( realpath "$0" ) && dirname "$temp")"
KEYSTORE_PATH="$SCRIPT_DIR/keystore.jks"
PROPERTIES_PATH="$SCRIPT_DIR/publish_key.properties"
echo Keystore path : $KEYSTORE_PATH
echo Properties path : $PROPERTIES_PATH
[ ! -n "$JKS_KEYSTORE" ] && echo 'Missing JKS_KEYSTORE variable!'&& exit 1
[ ! -n "$JKS_KEYSTORE_PASSWORD" ] && echo 'Missing JKS_KEYSTORE_PASSWORD variable!' && exit 1
# Write keystore
echo $JKS_KEYSTORE | base64 -d > "$KEYSTORE_PATH"
# Write keystore config
cat > "$PROPERTIES_PATH" <<_EOF
storePassword=$JKS_KEYSTORE_PASSWORD
keyPassword=$JKS_KEYSTORE_PASSWORD
keyAlias=moneymgr
storeFile=$KEYSTORE_PATH
_EOF

View File

@@ -0,0 +1,4 @@
storePassword=<password-from-previous-step>
keyPassword=<password-from-previous-step>
keyAlias=upload
storeFile=<keystore-file-location>

View File

@@ -1,43 +1,29 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/ocr_utils.dart';
import 'package:moneymgr_mobile/utils/pdf_utils.dart';
import 'package:moneymgr_mobile/widgets/expense_editor.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:scanbot_sdk/scanbot_sdk.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
part 'scan_screen.g.dart';
/// Scan a document & return generated PDF as byte file
@riverpod
Future<Uint8List?> _scanDocument(Ref ref) async {
var configuration = DocumentScanningFlow(
appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK,
),
cleanScanningSession: true,
outputSettings: DocumentScannerOutputSettings(pagesScanLimit: 1),
screens: DocumentScannerScreens(
review: ReviewScreenConfiguration(enabled: false),
),
);
var documentResult = await ScanbotSdkUiV2.startDocumentScanner(configuration);
Future<(Uint8List?, BaseExpenseInfo?)> _scanDocument(Ref ref) async {
final prefs = ref.watch(prefsProvider).requireValue;
if (documentResult.status != OperationStatus.OK) {
throw Exception("Scanner failed with status ${documentResult.status}");
}
// Convert result to PDF
var result = await ScanbotSdk.document.createPDFForDocument(
PDFFromDocumentParams(
documentID: documentResult.data!.uuid,
pdfConfiguration: PdfConfiguration(),
),
final pdf = await scanDocAsPDF();
final img = await renderPdf(pdfBytes: pdf);
final amount = await extractInfoFromBill(
imgBuff: img,
extractDates: !prefs.disableExtractDates(),
);
final pdfPath = result.pdfFileUri.replaceFirst("file://", "");
return File(pdfPath).readAsBytes();
return (pdf, amount);
}
class ScanScreen extends HookConsumerWidget {
@@ -45,7 +31,8 @@ class ScanScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final boredSuggestion = ref.watch(_scanDocumentProvider);
final scanDocProvider = ref.watch(_scanDocumentProvider);
final expenses = ref.watch(expensesProvider).requireValue;
restartScan() async {
try {
@@ -56,18 +43,25 @@ class ScanScreen extends HookConsumerWidget {
}
}
// Perform a switch-case on the result to handle loading/error states
return Padding(
padding: const EdgeInsets.all(8.0),
child: switch (boredSuggestion) {
AsyncData(:final value) when value != null => ExpenseEditor(
file: value,
onFinished: (e) {},
child: switch (scanDocProvider) {
AsyncData(:final value) when value.$1 != null => ExpenseEditor(
file: value.$1!,
initialData: value.$2,
onFinished: (expense) async {
await expenses.add(
info: expense,
fileContent: value.$1!,
fileMimeType: "application/pdf",
);
restartScan();
},
onRescan: restartScan,
),
// No data
AsyncData(:final value) when value == null => ScanErrorScreen(
AsyncData(:final value) when value.$1 == null => ScanErrorScreen(
message: "No document scanned!",
onTryAgain: restartScan,
),
@@ -103,7 +97,7 @@ class ScanErrorScreen extends StatelessWidget {
Spacer(flex: 5),
Text("An error occurred while scanning"),
Spacer(flex: 1),
Text(message),
Text(message, textAlign: TextAlign.center),
Spacer(flex: 1),
MaterialButton(
onPressed: onTryAgain,

View File

@@ -0,0 +1,86 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/widgets/expense_editor.dart';
import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'scan_details.g.dart';
@riverpod
Future<(Expense, Uint8List)?> _getExpense(Ref ref, {required int id}) async {
final expProvider = ref.watch(expensesProvider).requireValue;
final expense = await expProvider.getById(id);
if (expense == null) return null;
final file = await expProvider.loadFile(expense);
return (expense, file);
}
class ScanDetailScreen extends HookConsumerWidget {
final int id;
const ScanDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final expenses = ref.watch(expensesProvider).requireValue;
final expense = ref.watch(_getExpenseProvider(id: id));
handleUpdate(BaseExpenseInfo newInfo) async {
try {
await expenses.updateExpense(expense.requireValue!.$1, newInfo);
if (context.mounted) {
context.pop();
}
ref.invalidate(expensesProvider);
} catch (e, s) {
Logger.root.warning("Failed to update expense! $e$s");
if (context.mounted) {
context.showTextSnackBar("Failed to update expense! $e");
}
}
}
handleDelete() async {
try {
await expenses.deleteExpense(expense.requireValue!.$1);
if (context.mounted) {
context.pop();
}
ref.invalidate(expensesProvider);
} catch (e, s) {
Logger.root.warning("Failed to delete expense! $e$s");
if (context.mounted) {
context.showTextSnackBar("Failed to delete expense! $e");
}
}
}
return switch (expense) {
AsyncData(:final value) when value == null => FullScreenError(
message: "Expense does not exists!",
error: 'NONE',
),
AsyncData(:final value) => ExpenseEditor(
file: value!.$2,
initialData: value.$1.baseExpense,
onFinished: handleUpdate,
onDelete: handleDelete,
),
AsyncError(:final error) => FullScreenError(
message: "Failed to load expense information!",
error: error.toString(),
),
_ => LoadingScaffold(title: "Expense $id"),
};
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/time_utils.dart';
import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
import 'package:moneymgr_mobile/widgets/synchronize_button.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'scans_list_screen.g.dart';
@riverpod
Future<ExpensesList> _expensesList(Ref ref) {
final expenses = ref.watch(expensesProvider).requireValue;
return expenses.getList();
}
class ScansListScreen extends HookConsumerWidget {
const ScansListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final expensesList = ref.watch(_expensesListProvider);
return switch (expensesList) {
AsyncData(:final value) => Scaffold(
appBar: AppBar(
title: Text("Expenses"),
actions: [value.isEmpty ? Container() : SynchronizeButton()],
),
body: _ExpensesList(list: value),
),
AsyncError(:final error) => FullScreenError(
message: "Failed to load the list of expenses",
error: error.toString(),
),
_ => const LoadingScaffold(title: "Expenses"),
};
}
}
class _ExpensesList extends StatelessWidget {
final ExpensesList list;
const _ExpensesList({required this.list});
@override
Widget build(BuildContext context) {
if (list.isEmpty) {
return const Center(
child: Text("There is no entry waiting for upload (yet)"),
);
}
return ListView.builder(
itemBuilder: (context, entryNum) {
final expense = list[entryNum];
return ListTile(
onTap: () => context.push("$scansPage/${expense.id}"),
leading: Icon(Icons.receipt_long),
title: Text(
expense.label ?? "No label",
style: TextStyle(
fontStyle: expense.label == null ? FontStyle.italic : null,
),
),
subtitle: Text(expense.dateTime.simpleDate),
trailing: Text("${expense.cost}", style: TextStyle(fontSize: 20)),
);
},
itemCount: list.length,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/providers/settings.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
class SettingsScreen extends ConsumerWidget {
@@ -8,6 +9,7 @@ class SettingsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(prefsProvider).requireValue;
final themeMode = ref.watch(currentThemeModeProvider);
void onTapThemeMode() =>
@@ -15,6 +17,16 @@ class SettingsScreen extends ConsumerWidget {
void onTapLicenses() => context.showAppLicensePage();
handleToggleStartScreen(v) async {
await prefs.setStartOnScansListScreen(v);
ref.invalidate(prefsProvider);
}
handleToggleDisableExtractDate(v) async {
await prefs.setDisableExtractDates(v);
ref.invalidate(prefsProvider);
}
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
@@ -25,6 +37,22 @@ class SettingsScreen extends ConsumerWidget {
trailing: Text(themeMode.label),
onTap: onTapThemeMode,
),
SwitchListTile(
value: prefs.startOnScansListScreen(),
onChanged: handleToggleStartScreen,
title: Text("Start on scans screen"),
subtitle: Text(
"Do not start camera automatically on application startup",
),
),
SwitchListTile(
value: prefs.disableExtractDates(),
onChanged: handleToggleDisableExtractDate,
title: Text("Do not extract dates"),
subtitle: Text(
"Do not attempt to extract dates from scanned expenses",
),
),
const Divider(),
ListTile(
leading: const Icon(Icons.info_outline),

View File

@@ -24,7 +24,11 @@ class ApiClient {
: client = Dio(BaseOptions(baseUrl: token.apiUrl));
/// Get Dio instance
Future<Response<T>> execute<T>(String uri, {String method = "GET"}) async {
Future<Response<T>> execute<T>(
String uri, {
String method = "GET",
Object? data,
}) async {
Logger.root.fine("Request on ${token.apiUrl} - URI $uri");
return client.request(
uri,
@@ -32,6 +36,7 @@ class ApiClient {
method: method,
headers: {apiTokenHeader: _genJWT(method, uri)},
),
data: data,
);
}

View File

@@ -0,0 +1,43 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/api/api_client.dart';
part 'files_api.freezed.dart';
part 'files_api.g.dart';
@freezed
abstract class UploadResult with _$UploadResult {
const factory UploadResult({required int id}) = _UploadResult;
factory UploadResult.fromJson(Map<String, dynamic> json) =>
_$UploadResultFromJson(json);
}
extension FilesApi on ApiClient {
/// Upload a file
Future<UploadResult> uploadFile({
required String filename,
required String mimeType,
required Uint8List bytes,
}) async {
final res = await execute(
"/file",
method: "POST",
data: FormData.fromMap({
"file": MultipartFile.fromBytes(
bytes,
filename: filename,
contentType: MediaType.parse(mimeType),
),
}),
);
Logger.root.fine("Successfully uploaded file with response=${res.data}");
return UploadResult.fromJson(res.data);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'api_client.dart';
part 'inbox_api.freezed.dart';
part 'inbox_api.g.dart';
@freezed
abstract class UpdateInboxEntryRequest with _$UpdateInboxEntryRequest {
const factory UpdateInboxEntryRequest({
// ignore: non_constant_identifier_names
required int file_id,
required int time,
required String? label,
required double? amount,
}) = _UpdateInboxEntryRequest;
factory UpdateInboxEntryRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateInboxEntryRequestFromJson(json);
}
extension InboxApi on ApiClient {
/// Create a new inbox entry
Future<void> createInboxEntry(UpdateInboxEntryRequest entry) async {
await execute("/inbox", method: "POST", data: entry.toJson());
}
}

View File

@@ -7,8 +7,11 @@ import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
import 'package:moneymgr_mobile/routes/scan_details/scan_details.dart';
import 'package:moneymgr_mobile/routes/scans_list/scans_list_screen.dart';
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/widgets/load_startup_data.dart';
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -27,6 +30,8 @@ GoRouter router(Ref ref) {
authStateNotifier.value = value;
});
final prefs = ref.read(prefsProvider).requireValue;
// This is the only place you need to define your navigation items. The items
// will be propagated automatically to the router and the navigation bar/rail
// of the scaffold.
@@ -35,12 +40,28 @@ GoRouter router(Ref ref) {
// see [AuthState] enum.
final navigationItems = [
NavigationItem(
path: scanPage,
path: capturePage,
body: (_) => ScanScreen(),
icon: Icons.camera_alt_outlined,
selectedIcon: Icons.camera_alt,
label: "Scan",
),
NavigationItem(
path: scansPage,
body: (_) => ScansListScreen(),
icon: Icons.list,
selectedIcon: Icons.list_alt,
label: "List",
routes: [
GoRoute(
path: ":id",
builder: (_, state) {
final id = int.parse(state.pathParameters["id"]!);
return ScanDetailScreen(id: id);
},
),
],
),
NavigationItem(
path: profilePage,
body: (_) => ProfileScreen(),
@@ -52,22 +73,22 @@ GoRouter router(Ref ref) {
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: navigationItems.first.path,
initialLocation: prefs.startOnScansListScreen() ? scansPage : capturePage,
routes: [
GoRoute(path: homePage, builder: (_, __) => const Scaffold()),
GoRoute(path: authPage, builder: (_, __) => const LoginScreen()),
GoRoute(path: qrAuthPath, builder: (_, __) => const QrAuthScreen()),
GoRoute(path: homePage, builder: (_, _) => const Scaffold()),
GoRoute(path: authPage, builder: (_, _) => const LoginScreen()),
GoRoute(path: qrAuthPath, builder: (_, _) => const QrAuthScreen()),
GoRoute(
path: manualAuthPage,
builder: (_, __) => const ManualAuthScreen(),
builder: (_, _) => const ManualAuthScreen(),
),
GoRoute(path: settingsPage, builder: (_, __) => const SettingsScreen()),
GoRoute(path: settingsPage, builder: (_, _) => const SettingsScreen()),
// Configuration for the bottom navigation bar routes. The routes themselves
// should be defined in [navigationItems]. Modification to this [ShellRoute]
// config is rarely needed.
ShellRoute(
builder: (_, __, child) => child,
builder: (_, _, child) => child,
routes: [
for (final (index, item) in navigationItems.indexed)
GoRoute(

View File

@@ -13,8 +13,11 @@ const manualAuthPage = "/login/manual";
/// Settings path
const settingsPage = "/settings";
/// Scan URL path
const scanPage = "/scan";
/// Scan path
const capturePage = "/scan";
/// Scans page
const scansPage = "/scans";
/// Profile path
const profilePage = "/profile";

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -15,11 +16,16 @@ typedef ExpensesList = List<Expense>;
@freezed
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
const BaseExpenseInfo._();
const factory BaseExpenseInfo({
required String label,
required int cost,
required String? label,
required double cost,
required DateTime time,
}) = _BaseExpenseInfo;
/// Get expense time as second
int get timeAsSeconds => (time.millisecondsSinceEpoch / 1000).floor();
}
@freezed
@@ -36,7 +42,7 @@ abstract class Expense with _$Expense {
/// The cost shall always be a positive value
required double cost,
/// Time associated with the expense
/// Time associated with the expense (seconds since epoch)
required int time,
/// Associated file mime type
@@ -46,6 +52,10 @@ abstract class Expense with _$Expense {
factory Expense.fromJson(Map<String, dynamic> json) =>
_$ExpenseFromJson(json);
/// Get base expense information
BaseExpenseInfo get baseExpense =>
BaseExpenseInfo(label: label, cost: cost, time: dateTime);
/// Get associated expense file name
String get localFileName {
if (mimeType == "application/pdf") return "$id.pdf";
@@ -53,6 +63,9 @@ abstract class Expense with _$Expense {
if (mimeType == "image/png") return "$id.png";
return id.toString();
}
/// Get expense date
DateTime get dateTime => DateTime.fromMillisecondsSinceEpoch(time * 1000);
}
@riverpod
@@ -80,21 +93,24 @@ class ExpensesManager {
/// Get the current list of expenses
Future<ExpensesList> getList() async {
// On first save the list does not exists.
if (!await expenseFile.exists()) {
return [];
}
final jsonDec = jsonDecode(await expenseFile.readAsString());
return List<Expense>.from(jsonDec.map((m) => Expense.fromJson(m)));
}
/// Save the list of expenses
Future<void> saveList(ExpensesList list) async {
final jsonDoc = jsonEncode(list.map((t) => t.toJson()));
final jsonDoc = jsonEncode(list.map((t) => t.toJson()).toList());
await expenseFile.writeAsString(jsonDoc);
}
/// Add a new expense to the list
Future<void> add({
required String? label,
required double cost,
required int time,
required BaseExpenseInfo info,
required List<int> fileContent,
required String fileMimeType,
}) async {
@@ -102,9 +118,9 @@ class ExpensesManager {
final exp = Expense(
id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000),
label: label,
cost: cost,
time: time,
label: info.label,
cost: info.cost,
time: info.timeAsSeconds,
mimeType: fileMimeType,
);
@@ -123,4 +139,45 @@ class ExpensesManager {
list.add(exp);
await saveList(list);
}
/// Get a single expense information by its ID
Future<Expense?> getById(int id) async {
final list = await getList();
return list.firstWhere((e) => e.id == id);
}
/// Load the file associated with an expense
Future<Uint8List> loadFile(Expense expense) async {
final path = p.join(filesStoragePath.absolute.path, expense.localFileName);
return File(path).readAsBytes();
}
/// Update expense information
Future<void> updateExpense(Expense expense, BaseExpenseInfo newInfo) async {
final list = await getList();
final entry = list.indexWhere((e) => e.id == expense.id);
list[entry] = Expense(
id: expense.id,
label: newInfo.label,
cost: newInfo.cost,
time: newInfo.timeAsSeconds,
mimeType: expense.mimeType,
);
saveList(list);
}
/// Delete an expense
Future<void> deleteExpense(Expense expense) async {
// Remove expense from the list
final list = await getList();
await saveList(list.where((e) => e.id != expense.id).toList());
// Delete associated file, if any
final filePath = File(
p.join(filesStoragePath.absolute.path, expense.localFileName),
);
if (await filePath.exists()) {
await filePath.delete();
}
}
}

View File

@@ -15,6 +15,22 @@ Future<SharedPreferencesWithCache> prefs(Ref ref) =>
);
extension MoneyMgrSharedPreferences on SharedPreferencesWithCache {
bool startOnScansListScreen() {
return getBool("startOnScansListScreen") ?? false;
}
Future<void> setStartOnScansListScreen(bool start) async {
await setBool("startOnScansListScreen", start);
}
bool disableExtractDates() {
return getBool("disableExtractDates") ?? false;
}
Future<void> setDisableExtractDates(bool disable) async {
await setBool("disableExtractDates", disable);
}
ServerConfig? serverConfig() {
final json = getString("serverConfig");
if (json != null) return ServerConfig.fromJson(jsonDecode(json));

View File

@@ -0,0 +1,86 @@
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
/// Attempt to extract information from invoice image
Future<BaseExpenseInfo?> extractInfoFromBill({
required Uint8List imgBuff,
required bool extractDates,
}) async {
final decodedImage = await decodeImageFromList(imgBuff);
final byteData = await decodedImage.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
final image = InputImage.fromBitmap(
bitmap: byteData!.buffer.asUint8List(),
width: decodedImage.width,
height: decodedImage.height,
);
final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
final extractionResult = await textRecognizer.processImage(image);
Logger.root.fine("Expense text: ${extractionResult.text}");
// Check for highestCost amount on invoice
final costRegexp = RegExp(
r'([0-9]+([ ]*(\\.|,)[ ]*[0-9]{1,2}){0,1})([ \\t\\n]*(EUR|eur|€)|E)',
multiLine: true,
caseSensitive: false,
);
var highestCost = 0.0;
for (final match in costRegexp.allMatches(extractionResult.text)) {
if (match.groupCount == 0) continue;
// Process only numeric value
final value = (match.group(1) ?? "").replaceAll(",", ".");
highestCost = max(highestCost, double.tryParse(value) ?? 0.0);
}
// Check for highestCost amount on invoice
final dateRegexp = RegExp(
r'([0-3][0-9])(\/|-)([0-1][0-9])(\/|-)((20|)[0-9]{2})',
multiLine: false,
caseSensitive: false,
);
final currDate = DateTime.now();
DateTime? newest;
for (final match in dateRegexp.allMatches(extractionResult.text)) {
if (match.groupCount < 6) continue;
int year = int.tryParse(match.group(5)!) ?? currDate.year;
try {
final date = DateTime(
year > 99 ? year : (2000 + year),
int.tryParse(match.group(3)!) ?? currDate.month,
int.tryParse(match.group(1)!) ?? currDate.day,
);
if (newest == null) {
newest = date;
} else {
newest = DateTime.fromMillisecondsSinceEpoch(
max(newest.millisecondsSinceEpoch, date.millisecondsSinceEpoch),
);
}
} catch (e, s) {
Logger.root.warning("Failed to parse date! $e$s");
}
}
return BaseExpenseInfo(
label: null,
cost: highestCost,
time: extractDates && (newest?.isBefore(currDate) ?? false)
? newest!
: currDate,
);
}

View File

@@ -0,0 +1,91 @@
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:pdf_image_renderer/pdf_image_renderer.dart';
import 'package:scanbot_sdk/scanbot_sdk.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
/// Scan document as PDF
Future<Uint8List> scanDocAsPDF() async {
var configuration = DocumentScanningFlow(
appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK,
),
cleanScanningSession: true,
outputSettings: DocumentScannerOutputSettings(pagesScanLimit: 1),
screens: DocumentScannerScreens(
review: ReviewScreenConfiguration(enabled: false),
),
);
var documentResult = await ScanbotSdkUiV2.startDocumentScanner(configuration);
if (documentResult.status != OperationStatus.OK) {
throw Exception("Scanner failed with status ${documentResult.status}");
}
// Convert result to PDF
var result = await ScanbotSdk.document.createPDFForDocument(
PDFFromDocumentParams(
documentID: documentResult.data!.uuid,
pdfConfiguration: PdfConfiguration(),
),
);
final pdfPath = result.pdfFileUri.replaceFirst("file://", "");
return File(pdfPath).readAsBytes();
}
/// Render PDF to image bits
Future<Uint8List> renderPdf({String? path, Uint8List? pdfBytes}) async {
assert(path != null || pdfBytes != null);
// Create temporary file if required
var isTemp = false;
if (path == null) {
path = p.join(
(await getTemporaryDirectory()).absolute.path,
"render-${Random().nextInt(10000).toString()}+.pdf",
);
await File(path).writeAsBytes(pdfBytes!);
isTemp = true;
}
try {
final pdf = PdfImageRenderer(path: path);
await pdf.open();
await pdf.openPage(pageIndex: 0);
// get the render size after the page is loaded
final size = await pdf.getPageSize(pageIndex: 0);
// get the actual image of the page
final img = await pdf.renderPage(
pageIndex: 0,
x: 0,
y: 0,
width: size.width,
// you can pass a custom size here to crop the image
height: size.height,
// you can pass a custom size here to crop the image
scale: 1,
// increase the scale for better quality (e.g. for zooming)
background: Colors.white,
);
// close the page again
await pdf.closePage(pageIndex: 0);
// close the PDF after rendering the page
pdf.close();
return img!;
} finally {
if (isTemp) {
await File(path).delete();
}
}
}

View File

@@ -1,36 +1,49 @@
import 'dart:typed_data';
import 'package:alert_dialog/alert_dialog.dart';
import 'package:confirm_dialog/confirm_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/time_utils.dart';
import 'package:moneymgr_mobile/widgets/pdf_viewer.dart';
import '../utils/hooks.dart';
class ExpenseEditor extends HookConsumerWidget {
final Uint8List file;
final Function(BaseExpenseInfo) onFinished;
final Future<void> Function(BaseExpenseInfo) onFinished;
final Function()? onRescan;
final Function()? onDelete;
final BaseExpenseInfo? initialData;
const ExpenseEditor({
super.key,
required this.file,
required this.onFinished,
required this.onRescan,
this.onRescan,
this.onDelete,
this.initialData,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
final labelController = useTextEditingController();
final costController = useTextEditingController();
final timeController = useState(DateTime.now());
final labelController = useTextEditingController(text: initialData?.label);
final costController = useTextEditingController(
text: initialData?.cost.toString(),
);
final timeController = useState(initialData?.time ?? DateTime.now());
final (:pending, :snapshot, :hasError) = useAsyncTask();
// Clear cost value
handleClearCost() {
costController.text = "";
}
// Pick a new date
handlePickDate() async {
@@ -50,13 +63,18 @@ class ExpenseEditor extends HookConsumerWidget {
return;
}
onFinished(
pending.value = onFinished(
BaseExpenseInfo(
label: labelController.text,
cost: int.tryParse(costController.text) ?? 0,
label: labelController.text.isEmpty ? null : labelController.text,
cost: double.tryParse(costController.text) ?? 0,
time: timeController.value,
),
);
// Reset screen after a scan
await pending.value;
labelController.text = "";
costController.text = "";
}
// Cancel operation
@@ -70,31 +88,64 @@ class ExpenseEditor extends HookConsumerWidget {
}
}
// Delete expense
handleDelete() async {
if (await confirm(
context,
content: Text("Do you really want to delete this expense?"),
) &&
onDelete != null) {
onDelete!();
}
}
// Open invoice in full screen
handleFullScreenInvoice() {
showDialog(
context: context,
builder: (c) => Scaffold(
appBar: AppBar(title: Text("Expense")),
body: SingleChildScrollView(
child: PDFViewer(pdfBytes: file, fit: BoxFit.fitWidth),
),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text("Expense info"),
actions: [
// Rescan expense
IconButton(
onPressed: onRescan == null ? null : handleRescan,
icon: Icon(Icons.restart_alt),
),
onRescan == null
? Container()
: IconButton(
onPressed: handleRescan,
icon: Icon(Icons.restart_alt),
),
// Delete expense
onDelete == null
? Container()
: IconButton(onPressed: handleDelete, icon: Icon(Icons.delete)),
// Submit
IconButton(onPressed: handleSubmit, icon: Icon(Icons.save)),
snapshot.connectionState == ConnectionState.waiting
? CircularProgressIndicator()
: IconButton(
onPressed: handleSubmit,
icon: Icon(Icons.save),
color: hasError ? Colors.red : null,
),
],
),
body: Column(
children: [
// Expense preview
Expanded(
child: PDFView(
pdfData: file,
onError: (e) {
Logger.root.warning("Failed to render PDF $e");
alert(context, content: Text("Failed to render PDF $e"));
},
fitPolicy: FitPolicy.BOTH,
child: GestureDetector(
onTap: handleFullScreenInvoice,
child: PDFViewer(pdfBytes: file, fit: BoxFit.contain),
),
),
@@ -103,8 +154,17 @@ class ExpenseEditor extends HookConsumerWidget {
// Cost
TextField(
controller: costController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Cost'),
keyboardType: TextInputType.numberWithOptions(
decimal: true,
signed: false,
),
decoration: InputDecoration(
labelText: 'Cost',
suffixIcon: IconButton(
onPressed: handleClearCost,
icon: const Icon(Icons.clear),
),
),
textInputAction: TextInputAction.done,
),

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class LoadingScaffold extends StatelessWidget {
final String title;
const LoadingScaffold({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: CircularProgressIndicator()),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/utils/pdf_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'pdf_viewer.g.dart';
@riverpod
Future<Uint8List> _renderPdf(
Ref ref, {
String? path,
Uint8List? pdfBytes,
}) async {
return renderPdf(path: path, pdfBytes: pdfBytes);
}
class PDFViewer extends ConsumerWidget {
final String? pdfPath;
final Uint8List? pdfBytes;
final double? width;
final double? height;
final BoxFit? fit;
const PDFViewer({
super.key,
this.pdfPath,
this.pdfBytes,
this.width,
this.height,
this.fit,
}) : assert(pdfPath != null || pdfBytes != null);
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(
_renderPdfProvider(path: pdfPath, pdfBytes: pdfBytes),
);
// Perform a switch-case on the result to handle loading/error states
return switch (provider) {
AsyncData(:final value) => Image(
image: MemoryImage(value),
width: width,
height: height,
fit: fit,
),
AsyncError(:final error) => Text('PDF error: $error'),
_ => Center(child: const CircularProgressIndicator()),
};
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/api/api_client.dart';
import 'package:moneymgr_mobile/services/api/files_api.dart';
import 'package:moneymgr_mobile/services/api/inbox_api.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/hooks.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'synchronize_button.g.dart';
/// Synchronize expenses list with backend
@riverpod
Future<void> _performSynchronization(Ref ref) async {
final expenses = ref.watch(expensesProvider).requireValue;
final apiService = ref.watch(apiServiceProvider)!;
final list = await expenses.getList();
for (final exp in list) {
// First, upload file
final bytes = await expenses.loadFile(exp);
final file = await apiService.uploadFile(
filename: exp.localFileName,
mimeType: exp.mimeType,
bytes: bytes,
);
// Then, create the inbox entry
await apiService.createInboxEntry(
UpdateInboxEntryRequest(
file_id: file.id,
time: exp.time,
label: exp.label,
amount: -1 * exp.cost,
),
);
// Lastly delete the local expense
ref.watch(expensesProvider).requireValue.deleteExpense(exp);
}
ref.invalidate(expensesProvider);
}
class SynchronizeButton extends HookConsumerWidget {
const SynchronizeButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final (:pending, :snapshot, :hasError) = useAsyncTask();
handleSynchronize() async {
try {
await ref.watch(
_performSynchronizationProvider.selectAsync((it) => it),
);
} catch (e, s) {
Logger.root.warning("Failed to synchronize expenses! $e $s");
if (context.mounted) {
context.showTextSnackBar("Failed to synchronize expenses! $e");
}
}
}
return snapshot.connectionState == ConnectionState.waiting
? Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: IconButton(
onPressed: () => pending.value = handleSynchronize(),
style: ButtonStyle(
backgroundColor: hasError
? WidgetStatePropertyAll(Colors.red)
: null,
),
icon: Icon(Icons.sync_rounded),
);
}
}

View File

@@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
url: "https://pub.dev"
source: hosted
version: "8.10.1"
version: "8.11.0"
characters:
dependency: transitive
description:
@@ -370,10 +370,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_native_splash:
dependency: "direct main"
description:
@@ -382,14 +382,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.6"
flutter_pdfview:
dependency: "direct main"
description:
name: flutter_pdfview
sha256: c402ad1f51ba8ea73b9fb04c003ca0a9286118ba5ac9787ee2aa58956b3fcf8a
url: "https://pub.dev"
source: hosted
version: "1.4.1+1"
flutter_riverpod:
dependency: transitive
description:
@@ -460,18 +452,18 @@ packages:
dependency: "direct dev"
description:
name: freezed
sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c"
sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.1.0"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.1.0"
frontend_server_client:
dependency: transitive
description:
@@ -492,10 +484,26 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf
sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
url: "https://pub.dev"
source: hosted
version: "15.2.4"
version: "16.0.0"
google_mlkit_commons:
dependency: transitive
description:
name: google_mlkit_commons
sha256: "8f40fbac10685cad4715d11e6a0d86837d9ad7168684dfcad29610282a88e67a"
url: "https://pub.dev"
source: hosted
version: "0.11.0"
google_mlkit_text_recognition:
dependency: "direct main"
description:
name: google_mlkit_text_recognition
sha256: "96173ad4dd7fd06c660e22ac3f9e9f1798a517fe7e48bee68eeec83853224224"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
graphs:
dependency: transitive
description:
@@ -537,7 +545,7 @@ packages:
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
dependency: "direct main"
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
@@ -612,10 +620,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "6.0.0"
logging:
dependency: "direct main"
description:
@@ -728,6 +736,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
pdf_image_renderer:
dependency: "direct main"
description:
name: pdf_image_renderer
sha256: "0ec76118b14663f17f9b6a8c29ec59cb1b82e466a3c16fbb2ed9f1b613fc41b7"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
petitparser:
dependency: transitive
description:
@@ -921,10 +937,10 @@ packages:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
version: "1.3.6"
source_span:
dependency: transitive
description:

View File

@@ -52,7 +52,7 @@ dependencies:
flutter_hooks: ^0.21.2
# Router
go_router: ^15.2.4
go_router: ^16.0.0
# Flutter extras widgets for columns and rows
flextras: ^1.0.0
@@ -72,6 +72,7 @@ dependencies:
# API requests
dio: ^5.8.0+1
http_parser: ^4.1.2
# Qr Code library
mobile_scanner: ^7.0.1
@@ -89,8 +90,11 @@ dependencies:
path_provider: ^2.1.5
path: ^1.9.1
# PDF viewer
flutter_pdfview: ^1.4.1+1
# PDF renderer
pdf_image_renderer: ^1.0.1
# Text extraction
google_mlkit_text_recognition: ^0.15.0
dev_dependencies:
flutter_test:
@@ -101,7 +105,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
flutter_lints: ^6.0.0
# Manage app icon
flutter_launcher_icons: ^0.14.4
@@ -126,6 +130,9 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
# Default Android flavor
default-flavor: development
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.

View File

@@ -9,28 +9,28 @@
"version": "0.0.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.6",
"@jsonjoy.com/base64": "^1.1.2",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
"@mui/x-charts": "^8.5.3",
"@mui/x-data-grid": "^8.5.3",
"@mui/x-date-pickers": "^8.5.3",
"@mui/x-charts": "^8.8.0",
"@mui/x-data-grid": "^8.8.0",
"@mui/x-date-pickers": "^8.8.0",
"date-and-time": "^3.6.0",
"dayjs": "^1.11.13",
"filesize": "^10.1.6",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.2",
"react-router-dom": "^7.6.2",
"react-router": "^7.6.3",
"react-router-dom": "^7.6.3",
"ts-pattern": "^5.7.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
@@ -38,8 +38,8 @@
"eslint-plugin-react-dom": "^1.49.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^00.4.20",
"eslint-plugin-react-x": "^1.52.2",
"globals": "^16.2.0",
"eslint-plugin-react-x": "^1.52.3",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
@@ -444,9 +444,9 @@
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"version": "11.14.1",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
@@ -1145,9 +1145,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
"integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1544,12 +1544,12 @@
}
},
"node_modules/@mui/types": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz",
"integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz",
"integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1"
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1561,14 +1561,14 @@
}
},
"node_modules/@mui/utils": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz",
"integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz",
"integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.3",
"@types/prop-types": "^15.7.14",
"@babel/runtime": "^7.27.6",
"@mui/types": "^7.4.4",
"@types/prop-types": "^15.7.15",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.1.0"
@@ -1591,15 +1591,16 @@
}
},
"node_modules/@mui/x-charts": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.3.tgz",
"integrity": "sha512-aLU3KNA5bfKufxCPxBYx34xOn1mY5xaYGxxImEIQhL1BDnsjdkeF7b7gitL62XHpJe7ceU0nr2PbAr8msU0ZBQ==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.8.0.tgz",
"integrity": "sha512-tqdwKoUpo8u+KZdEWO4C21Q0P3HOL/DadAZSMmTdtO1LDCO/m4S8UtHFpj2B0pZuikdiBJ5bz49I+nfBfK1Xng==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/utils": "^7.2.0",
"@mui/x-charts-vendor": "8.5.3",
"@mui/x-internals": "8.5.3",
"@mui/x-internal-gestures": "0.2.1",
"@mui/x-internals": "8.8.0",
"bezier-easing": "^2.1.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1652,14 +1653,14 @@
}
},
"node_modules/@mui/x-data-grid": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.5.3.tgz",
"integrity": "sha512-rA+de5yre16KFIGKRBUwb8kYIdn7SPPrZsBy1P3QxisqhC+Wz2AQg/W6WWv71aFHwplmGwsFUjU6d47Fy/wvXg==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.8.0.tgz",
"integrity": "sha512-xWoBmxHi5JvT0QvAYGYJYNy4DEi+Lez+lrsqw3YV7z0jEYyJoV9vjFCiFE4QmG6IPg62B1gZHYE5AkDLFCvPkw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/x-internals": "8.5.3",
"@mui/utils": "^7.2.0",
"@mui/x-internals": "8.8.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"use-sync-external-store": "^1.5.0"
@@ -1689,14 +1690,14 @@
}
},
"node_modules/@mui/x-date-pickers": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.5.3.tgz",
"integrity": "sha512-rToJOgIfVN/mG8QPX5RSPcP7QdnNN4yk+37fBztVUnmR7mhiWdUNSHlazGpnRGRqDdr4lXRkJbTjtJLviWkhKg==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.8.0.tgz",
"integrity": "sha512-Rlk1wgkNHjMf22Ejv6jB+XueFYZmiwMYlJz3oRw9d8HhnshtMVjJbSNOI9yZ2wtqyEr0CGfryCnryywHpmfzeA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/x-internals": "8.5.3",
"@mui/utils": "^7.2.0",
"@mui/x-internals": "8.8.0",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1754,14 +1755,23 @@
}
}
},
"node_modules/@mui/x-internal-gestures": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.2.1.tgz",
"integrity": "sha512-7Po6F4/RdUrFyRwiwvh5ZNeY/bi8wavTCUe+stKAyMliKpgcYiEtH7ywTgroOEq0o56fIpyPzwC4+bbGwYFnvA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
}
},
"node_modules/@mui/x-internals": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.3.tgz",
"integrity": "sha512-ImCg4E3DT3XoDIZO0pNCbB7iw14N+YCFY3J1V28POwCD7P2f3HSIz4jwzM006oYxI6bqeE6LMfpdPRDW6s6dQw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.8.0.tgz",
"integrity": "sha512-qTRK5oINkAjZ7sIHpSnESLNq1xtQUmmfmGscYUSEP0uHoYh6pKkNWH9+7yzggRHuTv+4011VBwN9s+efrk+xZg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"@mui/utils": "^7.2.0",
"reselect": "^5.1.1"
},
"engines": {
@@ -2234,9 +2244,9 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
@@ -2333,14 +2343,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz",
"integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.35.0",
"@typescript-eslint/types": "^8.35.0",
"@typescript-eslint/tsconfig-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2355,9 +2365,9 @@
}
},
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2387,9 +2397,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz",
"integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3487,29 +3497,28 @@
}
},
"node_modules/eslint-plugin-react-x": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-1.52.2.tgz",
"integrity": "sha512-Pxpf3YxCUcNgzJVT6blAJ2KvLX32pUxtXndaCZoTdiytFw/H9OZKq4Qczxx/Lpo9Ri5rm4FbIZL3BfL/HGmzBw==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-1.52.3.tgz",
"integrity": "sha512-Sds4CXHtdgaCdzoypcY3DSshS0JtK2Eh+QbpUAPUqs0UWQ3qtQKxY0nntTSYeF+GXDfOdAYDkl/8+VFpHQwIKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/ast": "1.52.2",
"@eslint-react/core": "1.52.2",
"@eslint-react/eff": "1.52.2",
"@eslint-react/kit": "1.52.2",
"@eslint-react/shared": "1.52.2",
"@eslint-react/var": "1.52.2",
"@typescript-eslint/scope-manager": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/ast": "1.52.3",
"@eslint-react/core": "1.52.3",
"@eslint-react/eff": "1.52.3",
"@eslint-react/kit": "1.52.3",
"@eslint-react/shared": "1.52.3",
"@eslint-react/var": "1.52.3",
"@typescript-eslint/scope-manager": "^8.36.0",
"@typescript-eslint/type-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"@typescript-eslint/utils": "^8.36.0",
"compare-versions": "^6.1.1",
"is-immutable-type": "^5.0.1",
"string-ts": "^2.2.1",
"ts-pattern": "^5.7.1"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
},
"peerDependencies": {
@@ -3530,123 +3539,117 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/ast": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-1.52.2.tgz",
"integrity": "sha512-L0Tbbzx5l7JHgkQ1TqPWQuZ4+PsXDcgtt3056FOYqstUrDRG+5ylm7h3gEWu98I3FDdgLS8q9dOzz0PGgwZCTA==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-1.52.3.tgz",
"integrity": "sha512-71afQeBz0t5FqxLPfOgfQy2703t4T4tM5ooF/swIfUljCQxrFvIYivzYU67wrwLSnmkSfFJKp99bUCz7L3IP4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/eff": "1.52.2",
"@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/typescript-estree": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/eff": "1.52.3",
"@typescript-eslint/types": "^8.36.0",
"@typescript-eslint/typescript-estree": "^8.36.0",
"@typescript-eslint/utils": "^8.36.0",
"string-ts": "^2.2.1",
"ts-pattern": "^5.7.1"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/core": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-1.52.2.tgz",
"integrity": "sha512-FpxKZJHlf3zXETNL+WQP/SoYuVQNheWm1iDgW68RyHygD8mzk9CnVLDgjMrfmh2n0eaOqnWCL/IC2YzD6VpYOQ==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-1.52.3.tgz",
"integrity": "sha512-N/fY3q1V0F81OzKGn0ZopmHY+OQHYQiS49MvpSWhNciL+TDxOo4CSt+wayMz5/9G/B/PwGB68eprjow0AaTYzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/ast": "1.52.2",
"@eslint-react/eff": "1.52.2",
"@eslint-react/kit": "1.52.2",
"@eslint-react/shared": "1.52.2",
"@eslint-react/var": "1.52.2",
"@typescript-eslint/scope-manager": "^8.34.0",
"@typescript-eslint/type-utils": "^8.34.0",
"@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/ast": "1.52.3",
"@eslint-react/eff": "1.52.3",
"@eslint-react/kit": "1.52.3",
"@eslint-react/shared": "1.52.3",
"@eslint-react/var": "1.52.3",
"@typescript-eslint/scope-manager": "^8.36.0",
"@typescript-eslint/type-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"@typescript-eslint/utils": "^8.36.0",
"birecord": "^0.1.1",
"ts-pattern": "^5.7.1"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/eff": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-1.52.2.tgz",
"integrity": "sha512-YBPE2J1+PfXrR9Ct+9rQsw8uRU06zHopI508cfj0usaIBf3hz18V2GoRTVhsjniP0QbvKQdHzyPmmS/B6uyMZQ==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-1.52.3.tgz",
"integrity": "sha512-CU07yUuHrrBbb8C82via3GrAXkSMbcpxd6f18f/jjEmMAXzKbN2yq1t0GfG7iwIyZexDZ7R3QBa9ksk6iwtDAA==",
"dev": true,
"license": "MIT",
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/kit": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/kit/-/kit-1.52.2.tgz",
"integrity": "sha512-k0cSgFnPlDPI1xyRzHjEWIapLG0zCy7mx1HBLg5wuKf/zzSh3iNFId53xMebR05vM2k9YH63gsvTwRkGx/77Zw==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/kit/-/kit-1.52.3.tgz",
"integrity": "sha512-IOsfaRSih7VdL9ZDjuqc7kjOlHOQOaK6hkSENK64dUcvcl6YwHk8/JXfV/glHTp3JxXrPSazBrnZKNXk0DzjKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/eff": "1.52.2",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/eff": "1.52.3",
"@typescript-eslint/utils": "^8.36.0",
"ts-pattern": "^5.7.1",
"zod": "^3.25.63"
"zod": "^4.0.5"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/shared": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-1.52.2.tgz",
"integrity": "sha512-YHysVcCfmBoxt2+6Ao4HdLPUYNSem70gy+0yzOQvlQFSsGhh+uifQ68SSa/2uJBWfNUm9xQlyDsr2raeO4BlgA==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-1.52.3.tgz",
"integrity": "sha512-+0/2SOkNxLKBtYVLx/BCNo5xTn+dxkzP6C63gQ2ehNudMAt3zf2DouD62cHSSbl+eSAgc0zWYg8ssm5ksLN4xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/eff": "1.52.2",
"@eslint-react/kit": "1.52.2",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/eff": "1.52.3",
"@eslint-react/kit": "1.52.3",
"@typescript-eslint/utils": "^8.36.0",
"ts-pattern": "^5.7.1",
"zod": "^3.25.63"
"zod": "^4.0.5"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@eslint-react/var": {
"version": "1.52.2",
"resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-1.52.2.tgz",
"integrity": "sha512-/7IYMPsmO0tIYqkqAVnkqB4eXeVBvgBL/a9hcGCO2eUSzslYzQHSzNPhIoPLD9HXng+0CWlT+KupOFIqP9a26A==",
"version": "1.52.3",
"resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-1.52.3.tgz",
"integrity": "sha512-i2dfgoH93MHJNXqzS0vYIIpI2e6djIfzdnpMRHUyBYjTHFSPapE7RhcHFrAVPUrd85cUxIPW3pkTKAhkhUhYeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-react/ast": "1.52.2",
"@eslint-react/eff": "1.52.2",
"@typescript-eslint/scope-manager": "^8.34.0",
"@typescript-eslint/types": "^8.34.0",
"@typescript-eslint/utils": "^8.34.0",
"@eslint-react/ast": "1.52.3",
"@eslint-react/eff": "1.52.3",
"@typescript-eslint/scope-manager": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"@typescript-eslint/utils": "^8.36.0",
"string-ts": "^2.2.1",
"ts-pattern": "^5.7.1"
},
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/scope-manager": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz",
"integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0"
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3657,14 +3660,14 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/type-utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz",
"integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.35.0",
"@typescript-eslint/utils": "8.35.0",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -3681,9 +3684,9 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/types": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz",
"integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3695,16 +3698,16 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz",
"integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.35.0",
"@typescript-eslint/tsconfig-utils": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/visitor-keys": "8.35.0",
"@typescript-eslint/project-service": "8.36.0",
"@typescript-eslint/tsconfig-utils": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -3724,16 +3727,16 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/utils": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz",
"integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/typescript-estree": "8.35.0"
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3748,13 +3751,13 @@
}
},
"node_modules/eslint-plugin-react-x/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz",
"integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.35.0",
"@typescript-eslint/types": "8.36.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3804,6 +3807,16 @@
"node": ">=10"
}
},
"node_modules/eslint-plugin-react-x/node_modules/zod": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz",
"integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
@@ -4265,9 +4278,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5210,9 +5223,9 @@
}
},
"node_modules/react-router": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz",
"integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -5232,12 +5245,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz",
"integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==",
"license": "MIT",
"dependencies": {
"react-router": "7.6.2"
"react-router": "7.6.3"
},
"engines": {
"node": ">=20.0.0"

View File

@@ -11,28 +11,28 @@
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.6",
"@jsonjoy.com/base64": "^1.1.2",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
"@mui/x-charts": "^8.5.3",
"@mui/x-data-grid": "^8.5.3",
"@mui/x-date-pickers": "^8.5.3",
"@mui/x-charts": "^8.8.0",
"@mui/x-data-grid": "^8.8.0",
"@mui/x-date-pickers": "^8.8.0",
"date-and-time": "^3.6.0",
"dayjs": "^1.11.13",
"filesize": "^10.1.6",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.2",
"react-router-dom": "^7.6.2",
"react-router": "^7.6.3",
"react-router-dom": "^7.6.3",
"ts-pattern": "^5.7.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
@@ -40,8 +40,8 @@
"eslint-plugin-react-dom": "^1.49.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^00.4.20",
"eslint-plugin-react-x": "^1.52.2",
"globals": "^16.2.0",
"eslint-plugin-react-x": "^1.52.3",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"

View File

@@ -2,9 +2,11 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import DriveFileMoveOutlineIcon from "@mui/icons-material/DriveFileMoveOutline";
import LinkOffIcon from "@mui/icons-material/LinkOff";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import ClearIcon from "@mui/icons-material/Clear";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
IconButton,
InputAdornment,
ListItemIcon,
ListItemText,
TextField,
@@ -392,6 +394,19 @@ function MovementsTable(p: {
setFilter(e.target.value);
}}
style={{ padding: "0px", flex: 1 }}
slotProps={{
input: {
endAdornment: filter.length > 0 && (
<InputAdornment position="end">
<Tooltip title="Clear current filter">
<IconButton size="small" onClick={() => { setFilter(""); }}>
<ClearIcon />
</IconButton>
</Tooltip>
</InputAdornment>
),
},
}}
/>
<span style={{ flex: 1 }}></span>
<Tooltip title="Refresh table">