Compare commits
19 Commits
80977ee896
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
77590b4b4a
|
|||
|
1f164555be
|
|||
|
32b8d50e5b
|
|||
|
9b24a1d737
|
|||
|
8a6dd58007
|
|||
|
1b6749ef53
|
|||
|
cc208dd748
|
|||
|
e1dc42c254
|
|||
|
490d6aa650
|
|||
|
9d2ac48e1a
|
|||
|
a75e6232be
|
|||
|
af15a352a3
|
|||
|
e3618c539e
|
|||
|
9df89d0ffc
|
|||
|
4959cf1085
|
|||
|
f9cfdbd4a0
|
|||
|
3a5ff132ed
|
|||
|
90b24ec865
|
|||
|
6a557cc060
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -159,6 +159,7 @@ web/node_modules
|
||||
web/dist
|
||||
web/dist-ssr
|
||||
web/*.local
|
||||
web/dev-dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
466
package-lock.json
generated
466
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"classnames": "^2.5.1",
|
||||
"commander": "^13.1.0",
|
||||
"express": "^5.1.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@@ -31,19 +32,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@actual-app/api": {
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@actual-app/api/-/api-25.4.0.tgz",
|
||||
"integrity": "sha512-Amxn18rKfUDrLyebQ040NITAbymB2k+REd/XP1lnh99VYxakqLY6MHAH8WNLLfgIJETBZAf5mKYYCBOv05t2gg==",
|
||||
"version": "25.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@actual-app/api/-/api-25.9.0.tgz",
|
||||
"integrity": "sha512-AuR86F38lmeSEaRMe7fbN3jZj2PhfTFis1ftJdpMu98jdZv8XVQkcRJkBeaMZ3q2rSZ/uF7amnxguhME3MiWSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "^2.1.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@actual-app/crdt": {
|
||||
@@ -57,10 +58,23 @@
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actual-app/crdt/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
|
||||
"integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
|
||||
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -75,9 +89,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
|
||||
"integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
|
||||
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -92,9 +106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -109,9 +123,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -126,9 +140,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -143,9 +157,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -160,9 +174,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -177,9 +191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -194,9 +208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
|
||||
"integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
|
||||
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -211,9 +225,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -228,9 +242,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
|
||||
"integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
|
||||
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -245,9 +259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
|
||||
"integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
|
||||
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -262,9 +276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
|
||||
"integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
|
||||
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -279,9 +293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
|
||||
"integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
|
||||
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -296,9 +310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
|
||||
"integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
|
||||
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -313,9 +327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
|
||||
"integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
|
||||
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -330,9 +344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -347,9 +361,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -364,9 +378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -381,9 +395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -398,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -414,10 +428,27 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -432,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
|
||||
"integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
|
||||
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -449,9 +480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
|
||||
"integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
|
||||
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -466,9 +497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
|
||||
"integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
|
||||
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -521,9 +552,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -540,9 +571,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
||||
"integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -551,9 +582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
|
||||
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
|
||||
"integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -563,9 +594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
@@ -575,36 +606,36 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "1.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
|
||||
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
|
||||
"integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
|
||||
"integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
|
||||
"version": "22.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz",
|
||||
"integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/papaparse": {
|
||||
"version": "5.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz",
|
||||
"integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==",
|
||||
"version": "5.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.16.tgz",
|
||||
"integrity": "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
@@ -614,9 +645,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
@@ -624,9 +655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
|
||||
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -698,14 +729,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.9.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz",
|
||||
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
|
||||
"version": "12.4.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
|
||||
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
@@ -898,6 +932,12 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
@@ -983,9 +1023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1033,9 +1073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
|
||||
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1084,9 +1124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
@@ -1123,9 +1163,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
|
||||
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
|
||||
"version": "0.25.10",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
|
||||
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1136,31 +1176,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.3",
|
||||
"@esbuild/android-arm": "0.25.3",
|
||||
"@esbuild/android-arm64": "0.25.3",
|
||||
"@esbuild/android-x64": "0.25.3",
|
||||
"@esbuild/darwin-arm64": "0.25.3",
|
||||
"@esbuild/darwin-x64": "0.25.3",
|
||||
"@esbuild/freebsd-arm64": "0.25.3",
|
||||
"@esbuild/freebsd-x64": "0.25.3",
|
||||
"@esbuild/linux-arm": "0.25.3",
|
||||
"@esbuild/linux-arm64": "0.25.3",
|
||||
"@esbuild/linux-ia32": "0.25.3",
|
||||
"@esbuild/linux-loong64": "0.25.3",
|
||||
"@esbuild/linux-mips64el": "0.25.3",
|
||||
"@esbuild/linux-ppc64": "0.25.3",
|
||||
"@esbuild/linux-riscv64": "0.25.3",
|
||||
"@esbuild/linux-s390x": "0.25.3",
|
||||
"@esbuild/linux-x64": "0.25.3",
|
||||
"@esbuild/netbsd-arm64": "0.25.3",
|
||||
"@esbuild/netbsd-x64": "0.25.3",
|
||||
"@esbuild/openbsd-arm64": "0.25.3",
|
||||
"@esbuild/openbsd-x64": "0.25.3",
|
||||
"@esbuild/sunos-x64": "0.25.3",
|
||||
"@esbuild/win32-arm64": "0.25.3",
|
||||
"@esbuild/win32-ia32": "0.25.3",
|
||||
"@esbuild/win32-x64": "0.25.3"
|
||||
"@esbuild/aix-ppc64": "0.25.10",
|
||||
"@esbuild/android-arm": "0.25.10",
|
||||
"@esbuild/android-arm64": "0.25.10",
|
||||
"@esbuild/android-x64": "0.25.10",
|
||||
"@esbuild/darwin-arm64": "0.25.10",
|
||||
"@esbuild/darwin-x64": "0.25.10",
|
||||
"@esbuild/freebsd-arm64": "0.25.10",
|
||||
"@esbuild/freebsd-x64": "0.25.10",
|
||||
"@esbuild/linux-arm": "0.25.10",
|
||||
"@esbuild/linux-arm64": "0.25.10",
|
||||
"@esbuild/linux-ia32": "0.25.10",
|
||||
"@esbuild/linux-loong64": "0.25.10",
|
||||
"@esbuild/linux-mips64el": "0.25.10",
|
||||
"@esbuild/linux-ppc64": "0.25.10",
|
||||
"@esbuild/linux-riscv64": "0.25.10",
|
||||
"@esbuild/linux-s390x": "0.25.10",
|
||||
"@esbuild/linux-x64": "0.25.10",
|
||||
"@esbuild/netbsd-arm64": "0.25.10",
|
||||
"@esbuild/netbsd-x64": "0.25.10",
|
||||
"@esbuild/openbsd-arm64": "0.25.10",
|
||||
"@esbuild/openbsd-x64": "0.25.10",
|
||||
"@esbuild/openharmony-arm64": "0.25.10",
|
||||
"@esbuild/sunos-x64": "0.25.10",
|
||||
"@esbuild/win32-arm64": "0.25.10",
|
||||
"@esbuild/win32-ia32": "0.25.10",
|
||||
"@esbuild/win32-x64": "0.25.10"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
@@ -1413,9 +1454,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
|
||||
"integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1523,6 +1564,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -1768,6 +1818,7 @@
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
@@ -1861,9 +1912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||
"version": "3.77.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
|
||||
"integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
@@ -1963,9 +2014,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
@@ -1978,12 +2029,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
@@ -2068,9 +2120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@@ -2133,18 +2185,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
||||
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"iconv-lite": "0.7.0",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
@@ -2284,9 +2352,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -2466,9 +2534,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -2507,9 +2575,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
||||
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
@@ -2571,9 +2639,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsc-alias": {
|
||||
"version": "1.8.15",
|
||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.15.tgz",
|
||||
"integrity": "sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ==",
|
||||
"version": "1.8.16",
|
||||
"resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz",
|
||||
"integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2603,9 +2671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.3",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
|
||||
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
|
||||
"version": "4.20.6",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
||||
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2655,9 +2723,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -2690,16 +2758,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
@@ -2736,15 +2804,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
||||
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
|
||||
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"classnames": "^2.5.1",
|
||||
"commander": "^13.1.0",
|
||||
"express": "^5.1.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
|
||||
@@ -10,7 +10,7 @@ in
|
||||
pname = "actual-importer";
|
||||
version = "0.0.1";
|
||||
src = ./.;
|
||||
npmDepsHash = "sha256-ayfvTn8FVo7/K/Gy7oTDQiXDewvrU7B5JsNLHfrxibQ=";
|
||||
npmDepsHash = "sha256-SCXZr/Lpyzx8RoUnaM9hsBe5bLJ2xxRzruQv/2Lbetg=";
|
||||
|
||||
postInstall = ''
|
||||
mkdir -p $out/lib/node_modules/actual-importer/public
|
||||
|
||||
@@ -14,9 +14,9 @@ export type SubmitOptions =
|
||||
|
||||
type ActualTransaction = {
|
||||
id?: string;
|
||||
account?: string;
|
||||
account: string;
|
||||
date: string;
|
||||
amount?: number;
|
||||
amount: number;
|
||||
payee?: string;
|
||||
payee_name?: string;
|
||||
imported_payee?: string;
|
||||
@@ -51,7 +51,7 @@ export class Actual {
|
||||
}
|
||||
|
||||
#map(transaction: Transaction, accounts: ActualAccount[], payees: ActualPayee[]): ActualTransaction {
|
||||
const actualTransaction: ActualTransaction = {
|
||||
const actualTransaction: Omit<ActualTransaction, 'account'> & { account?: string } = {
|
||||
imported_id: transaction.id,
|
||||
date: transaction.date,
|
||||
amount: utils.amountToInteger(transaction.amount),
|
||||
@@ -94,7 +94,7 @@ export class Actual {
|
||||
break;
|
||||
}
|
||||
|
||||
return actualTransaction;
|
||||
return actualTransaction as ActualTransaction;
|
||||
}
|
||||
|
||||
async #api<T>(fn: () => Promise<T>): Promise<T> {
|
||||
@@ -186,6 +186,74 @@ export class Actual {
|
||||
return this.#doSubmit(prepared, opts);
|
||||
});
|
||||
}
|
||||
|
||||
async getTransactions(limit: number): Promise<Transaction[]> {
|
||||
return this.#api(async () => {
|
||||
const accounts: ActualAccount[] = await api.getAccounts();
|
||||
const payees: ActualPayee[] = await api.getPayees();
|
||||
|
||||
const query = api.q('transactions')
|
||||
.limit(limit)
|
||||
.select(['*'])
|
||||
.options({ splits: 'grouped' });
|
||||
|
||||
const { data } = await api.runQuery(query) as { data: ActualTransaction[] };
|
||||
|
||||
return this.#mapActualTransactionsToTransactions(data, accounts, payees);
|
||||
});
|
||||
}
|
||||
|
||||
async getTransactionsFromRange(start: string, end: string): Promise<Transaction[]> {
|
||||
return this.#api(async () => {
|
||||
const accounts: ActualAccount[] = await api.getAccounts();
|
||||
const payees: ActualPayee[] = await api.getPayees();
|
||||
|
||||
const query = api.q('transactions')
|
||||
.filter({ date: [{ $gte: start }, { $lte: end }] })
|
||||
.select(['*'])
|
||||
.options({ splits: 'grouped' });
|
||||
|
||||
const { data } = await api.runQuery(query) as { data: ActualTransaction[] };
|
||||
|
||||
return this.#mapActualTransactionsToTransactions(data, accounts, payees);
|
||||
});
|
||||
}
|
||||
|
||||
#mapActualTransactionsToTransactions(transactions: ActualTransaction[], accounts: ActualAccount[], payees: ActualPayee[]): Transaction[] {
|
||||
const transfers: string[] = [];
|
||||
|
||||
return transactions.map(t => {
|
||||
const account = accounts.find(a => a.id === t.account)?.name ?? t.account ?? "--unknown--";
|
||||
const payee = payees.find(p => p.id === t.payee)?.name ?? t.payee ?? "--unknown--";
|
||||
|
||||
let from = account;
|
||||
let to = payee;
|
||||
|
||||
if (t.amount && t.amount > 0) {
|
||||
from = payee;
|
||||
to = account;
|
||||
}
|
||||
|
||||
if (t.transfer_id) {
|
||||
if (transfers.includes(t.id!!)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
transfers.push(t.transfer_id);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: t.transfer_id ? 'transfer' : 'regular',
|
||||
date: t.date,
|
||||
from,
|
||||
to,
|
||||
fromDetails: from,
|
||||
toDetails: to,
|
||||
amount: (t.amount ?? 0)/100,
|
||||
title: t.notes
|
||||
} as Transaction;
|
||||
}).filter(t => t !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
|
||||
@@ -15,6 +15,29 @@ export type ImportResult = PrepareResult & {
|
||||
result: ActualImportResult;
|
||||
};
|
||||
|
||||
export const getTransactions = async (server: string, config: Config, limit: number = 5): Promise<Transaction[]> => {
|
||||
const serverConfig = config.servers[server];
|
||||
|
||||
if(!serverConfig) {
|
||||
throw new Error(`Unknown server: ${server}`);
|
||||
}
|
||||
|
||||
const actualServer = new Actual(serverConfig);
|
||||
|
||||
return await actualServer.getTransactions(limit);
|
||||
};
|
||||
|
||||
export const getTransactionsFromRange = async (server: string, config: Config, start: string, end: string): Promise<Transaction[]> => {
|
||||
const serverConfig = config.servers[server];
|
||||
|
||||
if(!serverConfig) {
|
||||
throw new Error(`Unknown server: ${server}`);
|
||||
}
|
||||
|
||||
const actualServer = new Actual(serverConfig);
|
||||
|
||||
return await actualServer.getTransactionsFromRange(start, end);
|
||||
};
|
||||
|
||||
export const prepareTransactions = async (stream: Readable, profile: string, server: string, config: Config): Promise<PrepareResult> => new Promise((resolve, reject) => {
|
||||
const profileConfig = config.profiles[profile];
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { prepareTransactions, submitTransactions } from "@/runner";
|
||||
import { getTransactions, getTransactionsFromRange, prepareTransactions, submitTransactions } from "@/runner";
|
||||
import { Config } from "@/types/config";
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import { Readable } from "stream";
|
||||
import path from 'path';
|
||||
import { Transaction } from "@/types/transaction";
|
||||
|
||||
|
||||
export function serve(config: Config, port: number) {
|
||||
@@ -20,6 +21,27 @@ export function serve(config: Config, port: number) {
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/servers/:server/transactions", async (req, res) => {
|
||||
const { start, end } = req.query;
|
||||
let data: Transaction[]|undefined;
|
||||
|
||||
if (start && end) {
|
||||
data = await getTransactionsFromRange(req.params.server, config, start.toString(), end.toString());
|
||||
}
|
||||
|
||||
else {
|
||||
const limitParam = req.query.limit?.toString();
|
||||
const limit = (limitParam && Number.parseInt(limitParam)) || undefined;
|
||||
data = await getTransactions(req.params.server, config, limit);
|
||||
}
|
||||
|
||||
const profile = req.query.profile?.toString() || undefined;
|
||||
const supportedAccounts = profile !== undefined ? config.profiles[profile].supportedAccounts : undefined;
|
||||
const filterAccounts = supportedAccounts !== undefined ? (t: Transaction) => supportedAccounts.includes(t.from) || supportedAccounts.includes(t.to) : () => true;
|
||||
|
||||
res.json(data.filter(filterAccounts))
|
||||
});
|
||||
|
||||
app.post("/prepare", upload.single("file"), async (req, res) => {
|
||||
if (!req.file) {
|
||||
throw new Error("No file to upload");
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ProfileConfig = {
|
||||
encoding?: string;
|
||||
config?: ParserConfig;
|
||||
csv?: Record<string, unknown>;
|
||||
supportedAccounts?: string[];
|
||||
};
|
||||
|
||||
export type ParserConfig = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Actual Importer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
5235
web/package-lock.json
generated
5235
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/classnames": "^2.3.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
@@ -25,6 +27,7 @@
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ buildNpmPackage {
|
||||
pname = "actual-importer-frontend";
|
||||
version = "0.0.1";
|
||||
src = ./.;
|
||||
npmDepsHash = "sha256-+HDXY0d3gvXJWy4GPa6vCIqLnq1mCP8kimek5Zl8u80=";
|
||||
npmDepsHash = "sha256-Xh+zpYX8u+wKuvBjGc4hxUI2Ed4tiXKC3zk8kFExBbc=";
|
||||
}
|
||||
|
||||
17
web/src/hooks/useDebounce.ts
Normal file
17
web/src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number = 500): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
41
web/src/hooks/useLoader.ts
Normal file
41
web/src/hooks/useLoader.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export type UseLoaderReturnType<T extends any[], R> = {
|
||||
fn: (...args: T) => Promise<R>,
|
||||
loading: boolean;
|
||||
completed: boolean;
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
export function useLoader<T extends any[], R>(fn: (...args: T) => Promise<R>, deps: React.DependencyList = []) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [error, setError] = useState<unknown|undefined>(undefined);
|
||||
|
||||
const callback = useCallback(async (...args: T): Promise<R> => {
|
||||
setLoading(true);
|
||||
setCompleted(false);
|
||||
setError(undefined);
|
||||
|
||||
// Wait 1 tick
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
try {
|
||||
const value = await fn(...args);
|
||||
setCompleted(true);
|
||||
return value;
|
||||
} catch(e: unknown) {
|
||||
setError(e);
|
||||
throw e;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fn, ...deps]);
|
||||
|
||||
return {
|
||||
fn: callback,
|
||||
loading,
|
||||
completed,
|
||||
error
|
||||
};
|
||||
}
|
||||
@@ -9,3 +9,9 @@ createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import classNames from "classnames";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
export type LoadFileFormProps = {
|
||||
profiles: string[];
|
||||
servers: string[];
|
||||
loading: boolean;
|
||||
defaultProfile?: string;
|
||||
defaultServer?: string;
|
||||
onSubmit?: (csvFile: File, profile: string, server: string) => Promise<void>;
|
||||
@@ -14,7 +16,7 @@ type Form = {
|
||||
server?: string;
|
||||
};
|
||||
|
||||
export default function LoadFileForm({ profiles, servers, defaultProfile, defaultServer, onSubmit }: LoadFileFormProps) {
|
||||
export default function LoadFileForm({ profiles, servers, defaultProfile, defaultServer, onSubmit, loading }: LoadFileFormProps) {
|
||||
const fileInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [formData, setFormData] = useState<Form>({});
|
||||
@@ -36,22 +38,24 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
|
||||
return undefined;
|
||||
}, [formData.files]);
|
||||
|
||||
const isValid = useMemo(() => selectedFile && formData.profile && formData.server, [formData, selectedFile]);
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!selectedFile || !formData.profile || !formData.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
onSubmit?.(selectedFile, formData.profile, formData.server);
|
||||
}, [formData, formData, selectedFile, onSubmit]);
|
||||
}, [formData, selectedFile, onSubmit]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label className="label">CSV File</label>
|
||||
<div className="control">
|
||||
<div className={`file is-fullwidth ${!!selectedFile ? "has-name" : ""}`}>
|
||||
<div className="control">
|
||||
<div className={classNames("file", "is-fullwidth", { "has-name": selectedFile })}>
|
||||
<label className="file-label">
|
||||
<input className="file-input" ref={fileInput} type="file" accept=".csv" onChange={(e) => setFormData({...formData, files: e.target.files ?? undefined})} required />
|
||||
<span className="file-cta">
|
||||
@@ -95,9 +99,8 @@ export default function LoadFileForm({ profiles, servers, defaultProfile, defaul
|
||||
<div className="control">
|
||||
<button
|
||||
type="submit"
|
||||
className="button is-link is-fullwidth"
|
||||
>
|
||||
Load transactions
|
||||
className={classNames("button", "is-link", "is-fullwidth", { 'is-loading': loading })}
|
||||
disabled={!isValid || loading}>Load transactions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import LoadFileForm from './LoadFileForm';
|
||||
import { fetchConfig, loadTransactions } from '../../services/api.service';
|
||||
import { useStore } from '../../store/AppStore';
|
||||
import { useLoader } from '../../hooks/useLoader';
|
||||
|
||||
export default function LoadFilePage() {
|
||||
const { dispatch } = useStore();
|
||||
@@ -27,9 +28,9 @@ export default function LoadFilePage() {
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, [loadConfig]);
|
||||
}, [loadConfig]);
|
||||
|
||||
const handleSubmit = useCallback(async (csvFile: File, profile: string, server: string) => {
|
||||
const { fn: handleSubmit, loading } = useLoader(async (csvFile: File, profile: string, server: string) => {
|
||||
const data = await loadTransactions(csvFile, profile, server);
|
||||
|
||||
dispatch({
|
||||
@@ -47,7 +48,7 @@ export default function LoadFilePage() {
|
||||
offset: 1
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="columns mt-6">
|
||||
@@ -55,6 +56,7 @@ export default function LoadFilePage() {
|
||||
<div className="box">
|
||||
<h2 className="title is-4 has-text-centered">Import Transactions</h2>
|
||||
<LoadFileForm
|
||||
loading={loading}
|
||||
profiles={profiles}
|
||||
servers={servers}
|
||||
defaultProfile={defaultProfile}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import classNames from "classnames";
|
||||
import type { Transaction } from "../../types/api";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export type ExistingTransactionsProps = {
|
||||
transactions: Transaction[];
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
const transactionEmoji = (transaction: Transaction): string => {
|
||||
if (transaction.kind === 'transfer') {
|
||||
return '➡️'
|
||||
};
|
||||
|
||||
return transaction.amount > 0 ? '⬇️' : '⬆️';
|
||||
};
|
||||
|
||||
const transactionType = (transaction: Transaction): string => {
|
||||
if (transaction.kind === 'transfer') {
|
||||
return 'Transfer'
|
||||
};
|
||||
|
||||
return transaction.amount > 0 ? 'Inflow' : 'Outflow';
|
||||
};
|
||||
|
||||
export function ExistingTransactions({ transactions, loading }: ExistingTransactionsProps) {
|
||||
const renderRow = useCallback((transaction: Transaction, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames('notification', 'is-light', {
|
||||
'is-success': transaction.kind === 'regular' && transaction.amount > 0,
|
||||
'is-danger': transaction.kind === 'regular' && transaction.amount < 0,
|
||||
'is-info': transaction.kind === 'transfer',
|
||||
})}>
|
||||
<h6 className="title is-6" title={transactionType(transaction)}>
|
||||
{transactionEmoji(transaction)} {transaction.date}
|
||||
</h6>
|
||||
<strong>From:</strong> {transaction.from}<br />
|
||||
<strong>To:</strong> {transaction.to}<br />
|
||||
<strong>Title:</strong> {transaction.title}<br />
|
||||
<strong>Amount:</strong> {transaction.amount}
|
||||
</div>
|
||||
), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading
|
||||
? <div className="notification is-warning is-light">
|
||||
<strong>Loading...</strong>
|
||||
</div>
|
||||
: transactions.map(renderRow).toReversed()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
121
web/src/pages/PrepareTransactionsPage/FilterPanel.tsx
Normal file
121
web/src/pages/PrepareTransactionsPage/FilterPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export type FilterData = {
|
||||
from?: Dayjs;
|
||||
to?: Dayjs;
|
||||
};
|
||||
|
||||
export type FilterPanelProps = {
|
||||
value?: FilterData;
|
||||
setValue: (value: FilterData) => void;
|
||||
};
|
||||
|
||||
const str2dayjs = (text?: string) => text ? dayjs(text) : undefined;
|
||||
const dayjs2str = (date?: Dayjs) => !date ? "" : date.format("YYYY-MM-DD");
|
||||
|
||||
export function FilterPanel({ value, setValue }: FilterPanelProps) {
|
||||
const handleChange = useCallback(<K extends keyof FilterData>(k: K, v: FilterData[K]) => {
|
||||
setValue({ ...value, [k]: v });
|
||||
}, [value]);
|
||||
|
||||
const handleQuickDate = useCallback((key: 'from'|'to', days: number) => {
|
||||
const date = dayjs().subtract(days, 'days');
|
||||
|
||||
setValue({
|
||||
...value,
|
||||
[key]: date
|
||||
});
|
||||
}, [value, setValue]);
|
||||
|
||||
return (<>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Date range</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={dayjs2str(value?.from)}
|
||||
onChange={e => handleChange('from', str2dayjs(e.target.value))}
|
||||
/>
|
||||
<p className="help"><strong>From</strong> (transactions not earlier than...)</p>
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('from', 0)}>
|
||||
Today
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('from', 1)}>
|
||||
Yesterday
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('from', 2)}>
|
||||
-2d
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('from', 3)}>
|
||||
-3d
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={dayjs2str(value?.to)}
|
||||
onChange={e => handleChange('to', str2dayjs(e.target.value))}
|
||||
/>
|
||||
<p className="help"><strong>To</strong> (transactions not later than...)</p>
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('to', 0)}>
|
||||
Today
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('to', 1)}>
|
||||
Yesterday
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('to', 2)}>
|
||||
-2d
|
||||
</button>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-dark is-outlined is-small" onClick={() => handleQuickDate('to', 3)}>
|
||||
-3d
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label"/>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<button className="button is-danger is-outlined" onClick={() => setValue({})}>
|
||||
Reset filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ImportOptions } from "../../types/api";
|
||||
import classNames from "classnames";
|
||||
|
||||
export type ImportBarProps = {
|
||||
onSubmit: (opts: ImportOptions) => void;
|
||||
onStartOver: () => void;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||
const [formData, setFormData] = useState<ImportOptions>({ mode: 'import' });
|
||||
export default function ImportBar({ onSubmit, loading, onStartOver }: ImportBarProps) {
|
||||
const [formData, setFormData] = useState<ImportOptions>({ mode: 'add' });
|
||||
|
||||
|
||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||
@@ -32,15 +35,15 @@ export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||
<div className="field">
|
||||
<label className="label">Import mode</label>
|
||||
<div className="control">
|
||||
<div className="buttons has-addons is-left">
|
||||
<div className="buttons has-addons is-left">
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${formData.mode === 'import' ? "is-primary is-selected" : ""}`}
|
||||
onClick={() => changeMode('import')}>Import</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${formData.mode === 'add' ? "is-primary is-selected" : ""}`}
|
||||
className={classNames("button", { 'is-primary': formData.mode === 'add', 'is-selected': formData.mode === 'add'})}
|
||||
onClick={() => changeMode('add')}>Add</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames("button", { 'is-primary': formData.mode === 'import', 'is-selected': formData.mode === 'import'})}
|
||||
onClick={() => changeMode('import')}>Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +72,18 @@ export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||
<div className="control">
|
||||
<button
|
||||
type="submit"
|
||||
className="button is-link is-fullwidth">Submit transactions</button>
|
||||
disabled={loading}
|
||||
className={classNames("button", "is-link", "is-fullwidth", { 'is-loading': loading })}>Submit transactions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<button
|
||||
type="button"
|
||||
className="button is-secondary is-fullwidth"
|
||||
onClick={onStartOver}>Start over again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,23 +1,62 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useStore } from "../../store/AppStore";
|
||||
import TransactionsTable from "./TransactionsTable";
|
||||
import { SkippedLines } from "./SkippedLines";
|
||||
import ImportBar from "./ImportBar";
|
||||
import type { ImportOptions } from "../../types/api";
|
||||
import { submitTransactions } from "../../services/api.service";
|
||||
import { type ImportOptions, type Transaction } from "../../types/api";
|
||||
import { getDateRangedTransactions, getLatestTransactions, submitTransactions } from "../../services/api.service";
|
||||
import { useLoader } from "../../hooks/useLoader";
|
||||
import { FilterPanel, type FilterData } from "./FilterPanel";
|
||||
import dayjs from "dayjs";
|
||||
import { ExistingTransactions } from "./ExistingTransactions";
|
||||
import classNames from "classnames";
|
||||
import { useDebounce } from "../../hooks/useDebounce";
|
||||
|
||||
|
||||
export default function PrepareTransactionsPage() {
|
||||
const { state, dispatch } = useStore();
|
||||
|
||||
const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]);
|
||||
|
||||
const handleSubmit = useCallback(async (opts: ImportOptions) => {
|
||||
const [userFilter, setUserFilter] = useState<FilterData>({});
|
||||
const debouncedUserFilter = useDebounce(userFilter, 700);
|
||||
const [deselected, setDeselected] = useState<number[]>([]);
|
||||
const [enabledExistingTransactions, setEnabledExistingTransactions] = useState(true);
|
||||
const [existingTransactions, setExistingTransactions] = useState<Transaction[]>([]);
|
||||
|
||||
const select = useCallback((index: number) => setDeselected(deselected.filter(x => x !== index)), [deselected, setDeselected]);
|
||||
const deselect = useCallback((index: number) => setDeselected([...deselected, index]), [deselected, setDeselected]);
|
||||
|
||||
const filteredTransactions = useMemo(() => state.transactions
|
||||
.filter(t => {
|
||||
if (!userFilter.from) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const date = dayjs(t.date);
|
||||
|
||||
return date.isAfter(userFilter.from, 'day') || date.isSame(userFilter.from, 'day');
|
||||
})
|
||||
.filter(t => {
|
||||
if (!userFilter.to) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const date = dayjs(t.date);
|
||||
|
||||
return date.isBefore(userFilter.to, 'day') || date.isSame(userFilter.to, 'day');
|
||||
})
|
||||
.slice().reverse()
|
||||
, [state.transactions, userFilter]);
|
||||
|
||||
const selectedTransactions = useMemo(() =>
|
||||
filteredTransactions.filter((_, i) => !deselected.includes(i)),
|
||||
[filteredTransactions, deselected]);
|
||||
|
||||
const { fn: handleSubmit, loading } = useLoader(async (opts: ImportOptions) => {
|
||||
if(!state.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await submitTransactions(desiredTransactions, state.server, opts);
|
||||
const response = await submitTransactions(selectedTransactions.slice().reverse(), state.server, opts);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_RESULT',
|
||||
@@ -30,17 +69,36 @@ export default function PrepareTransactionsPage() {
|
||||
offset: 1
|
||||
}
|
||||
});
|
||||
}, [desiredTransactions, state]);
|
||||
}, [selectedTransactions, state]);
|
||||
|
||||
const filterOut = useCallback((index: number) => dispatch({
|
||||
type: 'FILTER_OUT',
|
||||
payload: { index }
|
||||
}), [dispatch]);
|
||||
const { fn: refreshExistingTransactions, loading: loadingExistingTransactions } = useLoader(async () => {
|
||||
if (!state.server || !enabledExistingTransactions) {
|
||||
setExistingTransactions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const removeFilter = useCallback((index: number) => dispatch({
|
||||
type: 'REMOVE_FILTER',
|
||||
payload: { index }
|
||||
}), [dispatch]);
|
||||
// If no "from" filter active, pull the latest 10 transactions
|
||||
if (!debouncedUserFilter.from) {
|
||||
const transactions = await getLatestTransactions(state.server, 10, state.profile);
|
||||
setExistingTransactions(transactions);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = debouncedUserFilter.from.format("YYYY-MM-DD");
|
||||
const end = (debouncedUserFilter.to ?? dayjs()).format("YYYY-MM-DD");
|
||||
|
||||
const transactions = await getDateRangedTransactions(state.server, start, end, state.profile);
|
||||
setExistingTransactions(transactions);
|
||||
}, [enabledExistingTransactions, state.server, debouncedUserFilter]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setDeselected([])
|
||||
}, [filteredTransactions]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshExistingTransactions()
|
||||
}, [enabledExistingTransactions, state.server, debouncedUserFilter]);
|
||||
|
||||
return (
|
||||
<div className="columns mt-6">
|
||||
@@ -49,13 +107,50 @@ export default function PrepareTransactionsPage() {
|
||||
<h2 className="title is-4 has-text-centered">Prepare Transactions</h2>
|
||||
|
||||
<div className="content">
|
||||
<h3 className="title is-5">Considered transactions ({state.transactions.length})</h3>
|
||||
<TransactionsTable
|
||||
transactions={state.transactions}
|
||||
filteredOut={state.filteredOut}
|
||||
filterOut={filterOut}
|
||||
removeFilter={removeFilter} />
|
||||
</div>
|
||||
<h3 className="title is-5">Filters</h3>
|
||||
<FilterPanel value={userFilter} setValue={setUserFilter} />
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Fetch existing transactions</label>
|
||||
<div className="notification is-info is-light">
|
||||
If enabled, the existing transactions will be fetched automatically basing on date filter.
|
||||
If "from" is filled, the existing transactions will be fetched basing on set date (if "to" is not filled, the todays date will be considered).
|
||||
Otherwise, the latest <abbr title="Transaction before transfer consolidation.">10 raw transactions</abbr> will be fetched. Because of transfer consolidation,
|
||||
the ultimate number of transactions can be lower.
|
||||
</div>
|
||||
<div className="control">
|
||||
<div className="buttons has-addons">
|
||||
<button
|
||||
className={classNames('button', { 'is-selected': enabledExistingTransactions, 'is-success': enabledExistingTransactions})}
|
||||
onClick={() => setEnabledExistingTransactions(true)}
|
||||
disabled={enabledExistingTransactions}>I</button>
|
||||
<button
|
||||
className={classNames('button', { 'is-selected': !enabledExistingTransactions, 'is-danger': !enabledExistingTransactions})}
|
||||
onClick={() => setEnabledExistingTransactions(false)}
|
||||
disabled={!enabledExistingTransactions}>O</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className={classNames('column', { 'is-6': enabledExistingTransactions, 'is-12': !enabledExistingTransactions })}>
|
||||
<h3 className="title is-5">Considered transactions ({selectedTransactions.length}/{filteredTransactions.length})</h3>
|
||||
<TransactionsTable
|
||||
transactions={filteredTransactions}
|
||||
deselected={deselected}
|
||||
deselect={deselect}
|
||||
select={select} />
|
||||
</div>
|
||||
|
||||
{enabledExistingTransactions &&
|
||||
<div className="column is-6">
|
||||
<h3 className="title is-5">Existing transactions ({existingTransactions.length})</h3>
|
||||
<ExistingTransactions
|
||||
loading={loadingExistingTransactions}
|
||||
transactions={existingTransactions} />
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<h3 className="title is-5">Skipped lines ({state.skipped.length})</h3>
|
||||
@@ -63,9 +158,8 @@ export default function PrepareTransactionsPage() {
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<ImportBar onSubmit={handleSubmit} />
|
||||
<ImportBar loading={loading} onSubmit={handleSubmit} onStartOver={() => location.reload()}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.transactionTable tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled td {
|
||||
color: var(--bulma-text-light);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.transactionsTable {
|
||||
.transaction {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,72 @@
|
||||
import { useCallback } from "react";
|
||||
import type { Transaction } from "../../types/api";
|
||||
import styles from "./TransactionTable.module.css";
|
||||
import styles from "./TransactionsTable.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export type TransactionsTableProps = {
|
||||
transactions: Transaction[];
|
||||
filteredOut: number[];
|
||||
filterOut: (index: number) => void;
|
||||
removeFilter: (index: number) => void;
|
||||
deselected: number[];
|
||||
deselect: (index: number) => void;
|
||||
select: (index: number) => void;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export default function TransactionsTable({ transactions, filteredOut, filterOut, removeFilter, readonly }: TransactionsTableProps) {
|
||||
const transactionEmoji = (transaction: Transaction): string => {
|
||||
if (transaction.kind === 'transfer') {
|
||||
return '➡️'
|
||||
};
|
||||
|
||||
return transaction.amount > 0 ? '⬇️' : '⬆️';
|
||||
};
|
||||
|
||||
const transactionType = (transaction: Transaction): string => {
|
||||
if (transaction.kind === 'transfer') {
|
||||
return 'Transfer'
|
||||
};
|
||||
|
||||
return transaction.amount > 0 ? 'Inflow' : 'Outflow';
|
||||
};
|
||||
|
||||
export default function TransactionsTable({ transactions, deselected, deselect, select, readonly }: TransactionsTableProps) {
|
||||
|
||||
const changeSelection = useCallback((index: number) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(filteredOut.includes(index)) {
|
||||
removeFilter(index);
|
||||
if(deselected.includes(index)) {
|
||||
select(index);
|
||||
}
|
||||
|
||||
else {
|
||||
filterOut(index);
|
||||
deselect(index);
|
||||
}
|
||||
}, [filteredOut, filterOut, removeFilter]);
|
||||
|
||||
}, [deselected, deselect, select]);
|
||||
|
||||
const renderRow = useCallback((transaction: Transaction, index: number) => (
|
||||
<tr key={index} onClick={() => changeSelection(index)} className={`${!readonly && filteredOut.includes(index) ? styles.disabled : ""}`}>
|
||||
<td>{transaction.kind}</td>
|
||||
<td>{transaction.date}</td>
|
||||
<td>{transaction.from}</td>
|
||||
<td>{transaction.to}</td>
|
||||
<td>{transaction.amount}</td>
|
||||
<td>{transaction.title}</td>
|
||||
<td>{transaction.id}</td>
|
||||
</tr>
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => changeSelection(index)}
|
||||
className={classNames('notification', styles.transaction, {
|
||||
'is-success': transaction.kind === 'regular' && transaction.amount > 0,
|
||||
'is-danger': transaction.kind === 'regular' && transaction.amount < 0,
|
||||
'is-info': transaction.kind === 'transfer',
|
||||
'is-light': deselected.includes(index)
|
||||
})}>
|
||||
<h6 className="title is-6" title={transactionType(transaction)}>
|
||||
{transactionEmoji(transaction)} {transaction.date}
|
||||
</h6>
|
||||
{transaction.id && (<><strong>ID:</strong> {transaction.id}<br /></>)}
|
||||
<strong>From:</strong> {transaction.from}<br />
|
||||
<strong>To:</strong> {transaction.to}<br />
|
||||
<strong>Title:</strong> {transaction.title}<br />
|
||||
<strong>Amount:</strong> <span className={styles.amount}>{transaction.amount}</span>
|
||||
</div>
|
||||
), [changeSelection]);
|
||||
|
||||
return (
|
||||
<div className="table-container">
|
||||
<table className={`table is-hoverable ${!readonly ? styles.transactionTable : ""}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th>Kind</th>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Amount</th>
|
||||
<th>Title</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map(renderRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.transactionsTable}>
|
||||
{transactions.map(renderRow).toReversed()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default function ResultPage() {
|
||||
|
||||
return (
|
||||
<div className="columns mt-6">
|
||||
<div className="column is-8 is-offset-2">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<div className="box">
|
||||
<h2 className="title is-4 has-text-centered">Import Result</h2>
|
||||
<div className="content">
|
||||
@@ -40,6 +40,11 @@ export default function ResultPage() {
|
||||
<div className="content">
|
||||
{errors}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="button is-secondary is-fullwidth"
|
||||
onClick={() => location.reload()}>Start over again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,4 +38,32 @@ export async function submitTransactions(transactions: Transaction[], server: st
|
||||
|
||||
const data = await response.json();
|
||||
return data as SubmitResponse;
|
||||
}
|
||||
|
||||
export async function getLatestTransactions(server: string, limit: number = 5, profile?: string): Promise<Transaction[]> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', limit.toString());
|
||||
|
||||
if (profile !== undefined) {
|
||||
params.append('profile', profile);
|
||||
}
|
||||
|
||||
const response = await fetch(`/servers/${server}/transactions?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
return data as Transaction[];
|
||||
}
|
||||
|
||||
export async function getDateRangedTransactions(server: string, start: string, end: string, profile?: string): Promise<Transaction[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
params.append('start', start);
|
||||
params.append('end', end);
|
||||
|
||||
if (profile !== undefined) {
|
||||
params.append('profile', profile);
|
||||
}
|
||||
|
||||
const response = await fetch(`/servers/${server}/transactions?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
return data as Transaction[];
|
||||
}
|
||||
@@ -38,18 +38,6 @@ export function reducer(state: State, action: Action): State {
|
||||
}
|
||||
}
|
||||
|
||||
case 'FILTER_OUT':
|
||||
return {
|
||||
...state,
|
||||
filteredOut: [...state.filteredOut, action.payload.index]
|
||||
}
|
||||
|
||||
case 'REMOVE_FILTER':
|
||||
return {
|
||||
...state,
|
||||
filteredOut: state.filteredOut.filter(x => x !== action.payload.index)
|
||||
}
|
||||
|
||||
case 'SET_RESULT':
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -9,6 +9,4 @@ export const initialState: State = {
|
||||
wizard: {
|
||||
step: 0
|
||||
},
|
||||
|
||||
filteredOut: []
|
||||
};
|
||||
@@ -14,8 +14,6 @@ export type State = {
|
||||
step: number;
|
||||
};
|
||||
|
||||
filteredOut: number[];
|
||||
|
||||
result?: {
|
||||
errors?: {
|
||||
message: string;
|
||||
@@ -57,18 +55,6 @@ export type Action =
|
||||
offset: number;
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'FILTER_OUT',
|
||||
payload: {
|
||||
index: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'REMOVE_FILTER',
|
||||
payload: {
|
||||
index: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SET_RESULT',
|
||||
payload: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2023", "ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: {
|
||||
name: 'Actual Importer',
|
||||
short_name: 'ActualImporter',
|
||||
description: 'Simple app to import the transactions to Actual Budget system',
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/public/vite.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/svg',
|
||||
}
|
||||
]
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user