From 3225ebb9866b279f865e946ca25dc3cf55317e1a Mon Sep 17 00:00:00 2001 From: Casey Timm Date: Fri, 18 Jul 2025 23:02:01 -0400 Subject: [PATCH] kinda there --- .vscode/settings.json | 3 + package-lock.json | 162 ++++- package.json | 2 + src/hooks.server.js | 18 + src/lib/db.js | 578 ++++++++++++------ src/lib/editSymbol.svelte | 38 ++ src/lib/loadingModal.svelte | 13 + src/lib/settingsSymbol.svelte | 51 ++ src/lib/simplefin.js | 25 +- src/lib/trashbin.svelte | 18 + src/routes/+layout.server.js | 9 +- src/routes/+layout.svelte | 108 ++-- src/routes/account/[slug]/+page.server.js | 14 +- src/routes/account/[slug]/+page.svelte | 196 ++++-- src/routes/api/budget/[slug]/+server.js | 10 +- .../api/budget/[slug]/transaction/+server.js | 46 ++ src/routes/api/rules/+server.js | 19 + src/routes/api/rules/[slug]/+server.js | 21 + src/routes/api/simplefin/update/+server.js | 21 +- src/routes/budget/[slug]/+page.server.js | 5 +- src/routes/budget/[slug]/+page.svelte | 244 +++++--- src/routes/rules/+page.server.js | 10 + src/routes/rules/+page.svelte | 318 ++++++++++ src/routes/settings/+page.server.js | 10 +- src/routes/settings/+page.svelte | 171 +++++- src/routes/settings/deleteBudget.svelte | 23 + src/routes/transcation/[slug]/+server.js | 3 +- wipedata.psql | 4 + 28 files changed, 1665 insertions(+), 475 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/hooks.server.js create mode 100644 src/lib/editSymbol.svelte create mode 100644 src/lib/loadingModal.svelte create mode 100644 src/lib/settingsSymbol.svelte create mode 100644 src/lib/trashbin.svelte create mode 100644 src/routes/api/budget/[slug]/transaction/+server.js create mode 100644 src/routes/api/rules/+server.js create mode 100644 src/routes/api/rules/[slug]/+server.js create mode 100644 src/routes/rules/+page.server.js create mode 100644 src/routes/rules/+page.svelte create mode 100644 src/routes/settings/deleteBudget.svelte create mode 100644 wipedata.psql diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ae0bced --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "vscode-postgres.defaultConnection": "sql.caseytimm.com" +} diff --git a/package-lock.json b/package-lock.json index 4dd4396..d66784d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "budget", "version": "0.0.1", "dependencies": { + "@auth/sveltekit": "^1.10.0", "@tailwindcss/vite": "^4.1.11", + "cron": "^4.3.2", "postgres": "^3.4.7", "tailwindcss": "^4.1.11" }, @@ -37,6 +39,63 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/sveltekit": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@auth/sveltekit/-/sveltekit-1.10.0.tgz", + "integrity": "sha512-nTKS3FoFvgdqUwb7a8HZpLxDlx+pHndygcodM16J/iFHbe/0wha0MUCuTkVeUYZuKwL63L2ujmMAC1WEoki2+g==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.40.0", + "set-cookie-parser": "^2.7.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.3", + "@sveltejs/kit": "^1.0.0 || ^2.0.0", + "nodemailer": "^6.6.5", + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -497,11 +556,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -768,7 +835,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -788,7 +854,6 @@ "version": "2.22.2", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.2.tgz", "integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==", - "dev": true, "license": "MIT", "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", @@ -821,7 +886,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", - "dev": true, "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", @@ -843,7 +907,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.7" @@ -1139,7 +1202,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -1148,11 +1210,16 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1165,7 +1232,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1175,7 +1241,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1194,7 +1259,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1204,12 +1268,24 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/cron": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.2.tgz", + "integrity": "sha512-JxBBnf5zRz+NhW9XcP16gwUKAKIimy2G0QCCQu8kk5XwM4aCGwMt+nntouAfXF9A57965XzB6hitBlJAz5Ts6w==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.6.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1237,7 +1313,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1255,7 +1330,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1274,7 +1348,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "dev": true, "license": "MIT" }, "node_modules/enhanced-resolve": { @@ -1334,14 +1407,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "1.4.9", "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -1385,7 +1456,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -1400,11 +1470,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", + "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1642,7 +1720,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.castarray": { @@ -1666,6 +1743,15 @@ "dev": true, "license": "MIT" }, + "node_modules/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -1715,7 +1801,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -1725,7 +1810,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1735,7 +1819,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -1756,6 +1839,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/oauth4webapi": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.6.0.tgz", + "integrity": "sha512-OwXPTXjKPOldTpAa19oksrX9TYHA0rt+VcUFTkJ7QKwgmevPpNm9Cn5vFZUtIo96FiU6AfPuUUGzoXqgOzibWg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1829,6 +1921,25 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prettier": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", @@ -1899,7 +2010,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -1912,14 +2022,12 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "dev": true, "license": "MIT" }, "node_modules/sirv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", - "dev": true, "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -1943,7 +2051,6 @@ "version": "5.34.9", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.9.tgz", "integrity": "sha512-sld35zFpooaSRSj4qw8Vl/cyyK0/sLQq9qhJ7BGZo/Kd0ggYtEnvNYLlzhhoqYsYQzA0hJqkzt3RBO/8KoTZOg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -2017,7 +2124,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2108,7 +2214,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz", "integrity": "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==", - "dev": true, "license": "MIT", "workspaces": [ "tests/deps/*", @@ -2136,7 +2241,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true, "license": "MIT" } } diff --git a/package.json b/package.json index cda814f..303dc78 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "vite": "^6.2.6" }, "dependencies": { + "@auth/sveltekit": "^1.10.0", "@tailwindcss/vite": "^4.1.11", + "cron": "^4.3.2", "postgres": "^3.4.7", "tailwindcss": "^4.1.11" } diff --git a/src/hooks.server.js b/src/hooks.server.js new file mode 100644 index 0000000..c8a4797 --- /dev/null +++ b/src/hooks.server.js @@ -0,0 +1,18 @@ +import { CronJob } from 'cron'; +import { fetchAccounts } from '$lib/simplefin'; + +import type { Handle } from "@sveltejs/kit"; + +const job = new CronJob( + '10 0 * * * *', // cronTime + async function () { + const statDate = Math.floor(new Date(new Date().getFullYear(), new Date().getMonth()).getTime() / 1000); + const res = await fetchAccounts(startDate); + await updateAccounts(res); + await runRules(); + }, // onTick + null, // onComplete + true, // start + 'America/Detroit' // timeZone +); + diff --git a/src/lib/db.js b/src/lib/db.js index 0c532fe..7cf527b 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -1,173 +1,211 @@ // db.js -import postgres from 'postgres' +import postgres from 'postgres'; -const db = postgres({ host:'192.168.1.126', username:'budget', password:'budget', database:'budget'}) // will use psql environment variables +const db = postgres({ + host: '192.168.1.126', + username: 'budget', + password: 'budget', + database: 'budget' +}); // will use psql environment variables + +export { db }; + +export async function getTotal() { + const result = await db` + select sum(balance) as total + from account + where in_total is true + `; + // result = Result [{ total: 1000.00 }] + return result[0].total; +} export async function setAccountInTotal(accountId, total) { - return await db` + return await db` update account set in_total = ${total} where id = ${accountId} - ` + `; } export async function setAccountHide(accountId, hide) { - return await db` + return await db` update account set hide = ${hide} where id = ${accountId} - ` + `; +} + +export async function addBudget(name, notes) { + const result = await db` + insert into budget (name, notes) + values (${name}, ${notes}) + returning id + `; + // result = Result [{ id: 1 }] + return result[0].id; } export async function deleteBudget(id) { - return await db` - UPDATE budget - SET delete = true - WHERE id = ${id} - ` -} - -export async function createBudgetTable() { - return await db` - create table if not exists budget ( - id serial primary key, - name text not null, - amount numeric(10,2) not null, - notes text, - delete boolean default false - ) - ` -} - -export async function createBudgetTransactionTable() { - return await db` - create table if not exists budget_transaction ( - id serial primary key, - budget_id integer not null references budget(id), - transaction_id text not null references transaction(id), - amount numeric(10,2) not null - )` -} - -export async function addBudget(name, amount, notes) { - const result = await db` - insert into budget (name, amount, notes) - values (${name}, ${amount}, ${notes}) - returning id - ` - // result = Result [{ id: 1 }] - return result[0].id -} - -export async function deleteBudget(id) { - const result = await db` - delete from budget + const result = await db` + update budget + set delete = true where id = ${id} - ` - // result = Result [{ id: 1 }] - return result + `; + // result = Result [{ id: 1 }] + return result; +} + +export async function restoreBudget(id) { + const result = await db` + update budget + set delete = false + where id = ${id} + `; + // result = Result [{ id: 1 }] + return result; } export async function updateBudget(id, name, amount, notes) { - const result = await db` + const result = await db` update budget set name = ${name}, amount = ${amount}, notes = ${notes} where id = ${id} - ` - // result = Result [{ id: 1 }] - return result + `; + // result = Result [{ id: 1 }] + return result; } - export async function getBudgetTransactions(id) { - // Fetch all transactions associated with a specific budget - try { - const transactions = await db` + // Fetch all transactions associated with a specific budget + try { + let transactions = await db` select transaction.id as id, transaction.posted as posted, transaction.amount as amount, transaction.description as description, transaction.pending as pending, - transaction.notes as notes, + budget_transaction.notes as notes, + transaction.payee as payee, budget_transaction.amount as budget_amount, budget_transaction.id as budget_transaction_id from budget_transaction join transaction on budget_transaction.transaction_id = transaction.id where budget_transaction.budget_id = ${id} order by transaction.posted desc - ` - - // transactions = Result [{ id: 1, posted: 1633036800, amount: 50.00, description: "Grocery Store", pending: false, notes: "Weekly groceries" }, ...] - return { transactions } -} - catch { - await createBudgetTransactionTable(); - return [] - } + `; + console.log(`Fetched ${transactions.length} transactions for budget ${id}`); + transactions = transactions.map((t) => ({ + ...t, + date: new Date(t.posted * 1000) + })); + // transactions = Result [{ id: 1, posted: 1633036800, amount: 50.00, description: "Grocery Store", pending: false, notes: "Weekly groceries" }, ...] + return { transactions }; + } catch { + return []; + } } -export async function deleteBudgetTransaction(budgetId, transactionId) { - // Delete a transaction from a budget - const result = await db` +export async function updateBudgetTransaction(id, amount, notes) { + // Delete a transaction from a budget + const result = await db` + update from budget_transaction + where id= ${id} + SET amount = ${amount}, notes = ${notes} + `; + // result = Result [{ id: 1 }] + return result; +} + +export async function deleteBudgetTransaction(id) { + // Delete a transaction from a budget + const result = await db` delete from budget_transaction - where budget_id = ${budgetId} and transaction_id = ${transactionId} - ` - // result = Result [{ id: 1 }] - return result + where id = ${id} + `; + // result = Result [{ id: 1 }] + return result; } -export async function addBudgetTransaction(budgetId, transactionId, amount) { - // Add a transaction to a budget - const result = await db` - insert into budget_transaction (budget_id, transaction_id, amount) - values (${budgetId}, ${transactionId}, ${amount}) +export async function addBudgetTransaction(budgetId, transactionId, amount, notes, ruleId = null) { + const existingTransactions = await db` + select amount from budget_transaction + where transaction_id = ${transactionId} + `; + const realTransactionAmount = await db` + select amount from transaction + where id = ${transactionId} + `; + + if (existingTransactions.length > 0) { + if ( + existingTransactions.reduce((acc, curr) => acc + curr.amount, 0) + amount > + realTransactionAmount + ) { + return -1; + } + } + + // Add a transaction to a budget + const result = await db` + insert into budget_transaction (budget_id, transaction_id, amount, notes, rule_id) + values (${budgetId}, ${transactionId}, ${amount}, ${notes}, ${ruleId ?? null}) returning id - ` - // result = Result [{ id: 1 }] - return result[0].id + `; + // result = Result [{ id: 1 }] + return result[0].id; } export async function getBudgets() { - const budgets = await db` + const budgets = await db` select budget.id as id, budget.name as name, - budget.amount as amount, + budget.sum as sum, budget.notes as notes - from budget + from budget_with_sum as budget WHERE budget.delete is false - ` - if (!budgets) { - await createBudgetTable(); - return await getBudgets() - } - // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] - return budgets + `; + // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] + return budgets; +} + +export async function getBudget(id) { + const budget = await db` + select + budget.id as id, + budget.name as name, + budget.sum as sum, + budget.notes as notes, + budget.delete as delete + from budget_with_sum as budget + where budget.id = ${id} + `; + // budget = Result [{ id: 1, name: "Groceries", notes: "Monthly grocery budget" }] + if (!budget || budget.length === 0) { + return null; + } + return budget[0]; } export async function getDeletedBudgets() { - const budgets = await db` + const budgets = await db` select budget.id as id, budget.name as name, - budget.amount as amount, budget.notes as notes from budget WHERE budget.delete is true - ` - if (!budgets) { - await createBudgetTable(); - return await getBudgets() - } - // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] - return budgets + `; + // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] + return budgets; } export async function getAccount(id) { - const account = await db` + const account = await db` select account.id as id, account.name as name, @@ -184,73 +222,89 @@ export async function getAccount(id) { from account left join org on account.org_id = org.id where account.id = ${id} - ` - if (!account || account.length === 0) { - return null - } - return account[0]; + `; + if (!account || account.length === 0) { + return null; + } + return account[0]; } export async function getAccounts(age) { - const accounts = await db` + const accounts = await db` select account.id as id, account.name as name, balance from account where hide is false - ` - // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] - return accounts + `; + // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] + return accounts; +} + +export async function getBudgetTransactionsForAccount(accountID) { + const transactions = await db` + select budget_transaction.id as id, + budget_transaction.budget_id as budget_id, + budget_transaction.transaction_id as transaction_id, + budget_transaction.amount as amount, + budget_transaction.notes as notes + from budget_transaction + join transaction on budget_transaction.transaction_id = transaction.id + where transaction.account_id = ${accountID} + `; + return transactions; } export async function getHiddenAccounts(age) { - const accounts = await db` + const accounts = await db` select account.id as id, account.name as name from account where account.hide is true - ` - // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] - return accounts + `; + // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] + return accounts; } export async function getTransactions(accountId) { - let transactions = await db` + let transactions = await db` select transaction.id as id, transaction.posted as posted, transaction.amount as amount, transaction.description as description, transaction.pending as pending, - transaction.notes as notes + transaction.notes as notes, + transaction.payee as payee from transaction where account_id = ${accountId} order by posted desc - ` - transactions = transactions.map((t) => ({ - ...t, date: new Date(t.posted * 1000) - })); - return transactions + `; + transactions = transactions.map((t) => ({ + ...t, + date: new Date(t.posted * 1000) + })); + return transactions; } export async function setTransactionNote(transactionId, note) { - const result = await db` + const result = await db` update transaction set notes = ${note} where id = ${transactionId} - ` - return result + `; + return result; } export async function updateAccounts(data) { - try { - console.log('Updating accounts with data:', data); - for (const account of data.accounts) { - // Upsert Org - console.log(`Upserting org for account: ${account.id}`, account.org); - await db` + try { + console.log('Updating accounts with data:', data); + for (const account of data.accounts) { + // Upsert Org + console.log(`Upserting org for account: ${account.id}`, account.org); + await db` insert into org (id, domain, name, sfin_url, url) values (${account.org.id}, ${account.org.domain ?? null}, ${account.org.name ?? null}, ${account.org.sfin_url ?? null}, ${account.org.url ?? null}) on conflict (id) do update set @@ -259,9 +313,9 @@ export async function updateAccounts(data) { sfin_url = excluded.sfin_url, url = excluded.url `; - console.log(`Upserting account: ${account.id} (${account.name})`); - // Upsert Account - await db` + console.log(`Upserting account: ${account.id} (${account.name})`); + // Upsert Account + await db` insert into account (id, org_id, name, currency, balance, available_balance, balance_date) values ( ${account.id}, @@ -270,7 +324,7 @@ export async function updateAccounts(data) { ${account.currency ?? null}, ${account.balance ?? null}, ${account.available_balance ?? null}, - ${account.balance_date ?? null} + ${account.balance_date ?? null} ) on conflict (id) do update set org_id = excluded.org_id, @@ -280,35 +334,35 @@ export async function updateAccounts(data) { available_balance = excluded.available_balance, balance_date = excluded.balance_date `; - - // Upsert Transactions - if (account.transactions && account.transactions.length > 0) { - for (const txn of account.transactions) { - let extraId = null; - console.log(`Upserting transaction: ${txn.id} for account: ${account.id}`); - if (txn.extra) { - // Upsert TransactionExtra (insert only, update not needed for category) - const extraResult = await db` + + // Upsert Transactions + if (account.transactions && account.transactions.length > 0) { + for (const txn of account.transactions) { + let extraId = null; + console.log(`Upserting transaction: ${txn.id} for account: ${account.id}`); + if (txn.extra) { + // Upsert TransactionExtra (insert only, update not needed for category) + const extraResult = await db` insert into transaction_extra (category) values (${txn.extra.category ?? null}) on conflict (category) do nothing returning id `; - if (extraResult.length > 0) { - extraId = extraResult[0].id; - } else { - // If already exists, fetch id - const existing = await db` + if (extraResult.length > 0) { + extraId = extraResult[0].id; + } else { + // If already exists, fetch id + const existing = await db` select id from transaction_extra where category = ${txn.extra.category ?? null} `; - if (existing.length > 0) { - extraId = existing[0].id; - } - } - } - console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn); - await db` - insert into transaction (id, account_id, posted, amount, description, pending, transacted_at) + if (existing.length > 0) { + extraId = existing[0].id; + } + } + } + console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn); + await db` + insert into transaction (id, account_id, posted, amount, description, pending, transacted_at, payee) values ( ${txn.id}, ${account.id}, @@ -316,7 +370,8 @@ export async function updateAccounts(data) { ${txn.amount ?? null}, ${txn.description ?? null}, ${txn.pending ?? false}, - ${txn.transacted_at ?? 0} + ${txn.transacted_at ?? 0}, + ${txn.payee ?? null} ) on conflict (id) do update set account_id = excluded.account_id, @@ -324,17 +379,200 @@ export async function updateAccounts(data) { amount = excluded.amount, description = excluded.description, pending = excluded.pending, - transacted_at = excluded.transacted_at + transacted_at = excluded.transacted_at, + payee = excluded.payee `; - } - } - } - return true; - } catch (error) { - console.error('updateAccounts error:', error); - return false; - } + } + } + } + return true; + } catch (error) { + console.error('updateAccounts error:', error); + return false; + } } +export async function getRules(data) { + try { + const rules = await db` + select + id, + name, + description, + payee, + amount, + transdescription, + action, + priority, + action_amount, + account_id, + amount_is_precent, + use_priority + from rules + order by priority asc + `; + return rules; + } catch (error) { + console.error('getRules error:', error); + return []; + } +} -export default db \ No newline at end of file +export async function addRule( + name, + description, + payee, + amount, + transdescription, + action, + actionAmount, + account, + amount_is_precent, + priority, + use_priority +) { + try { + const result = await db` + insert into rules (name, description, payee, amount, transdescription, action, action_amount, account_id, amount_is_precent, priority, use_priority) + values (${name}, ${description}, ${payee}, ${amount}, ${transdescription}, ${action}, ${actionAmount}, ${account}, ${amount_is_precent}, ${priority}, ${use_priority}) + `; + return result; + } catch (error) { + console.error('addRule error:', error); + return null; + } +} + +export async function deleteRule(id) { + try { + const result = await db` + delete from rules + where id = ${id} + `; + return result; + } catch (error) { + console.error('deleteRule error:', error); + return null; + } +} + +export async function updateRule( + name, + description, + payee, + amount, + transdescription, + action, + actionAmount, + account, + amount_is_precent, + priority, + use_priority, + id +) { + console.log(`Updating rule with id: ${id}`); + try { + const result = await db` + update rules + set name = ${name}, + description = ${description}, + payee = ${payee}, + amount = ${amount}, + transdescription = ${transdescription}, + action = ${action}, + action_amount = ${actionAmount}, + account_id = ${account ?? null}, + amount_is_precent = ${amount_is_precent}, + priority = ${priority}, + use_priority = ${use_priority} + where id = ${id} + `; + return result; + } catch (error) { + console.error('updateRule error:', error); + console.log(` + update rules + set name = ${name}, + description = ${description}, + payee = ${payee}, + amount = ${amount}, + transdescription = ${transdescription}, + action = ${action}, + action_amount = ${actionAmount}, + account_id = ${account ?? null}, + amount_is_precent = ${amount_is_precent}, + use_priority = ${use_priority} + where id = ${id} + `); + return null; + } +} + +export async function runRules() { + try { + const rules = await getRules(); + let transactions = await db` + select id, account_id, payee, amount, description from transaction + `; + let budgetTransactions = await db` + select id, transaction_id, budget_id, notes, amount, rule_id from budget_transaction + `; + + console.log(`Running ${rules.length} rules on ${transactions.length} transactions`); + + for (const rule of rules.sort((a, b) => a.priority - b.priority)) { + console.log( + `Running rule: ${rule.name} (${rule.id}, ${rule.payee}, ${rule.amount}, ${rule.transdescription})` + ); + console.log( + `Rule: payee: ${rule.payee}, amount: ${rule.amount}, transdescription: ${rule.transdescription}, account: ${rule.account_id}` + ); + + const amountRE = new RegExp(rule.amount); + const descriptionRE = new RegExp(rule.transdescription); + console.log(rule.account_id); + const accountRE = new RegExp(rule.account_id === null ? '.*' : rule.account_id); + console.log(accountRE); + const payeeRE = new RegExp(rule.payee); + + for (const transaction of transactions) { + if ( + amountRE.test(transaction.amount) && + descriptionRE.test(transaction.description) && + accountRE.test(transaction.account_id) && + payeeRE.test(transaction.payee) + ) { + if ( + rule.action && + !budgetTransactions.some( + (bt) => bt.transaction_id === transaction.id && bt.rule_id === rule.id + ) + ) { + const amount = rule.amount_is_precent + ? (transaction.amount * rule.action_amount) / 100 + : rule.action_amount; + const notes = `Rule - ${rule.name}`; + + const budgetTransactionId = await addBudgetTransaction( + rule.action, + transaction.id, + amount, + notes, + rule.id + ); + await db` + update transaction set processed = true + where id = ${transaction.id}`; + console.log( + `Added budget transaction ${budgetTransactionId} for rule ${rule.name} on transaction ${transaction.id}` + ); + } + } + } + } + } catch (error) { + console.error('runRules error:', error); + } +} + +export default db; diff --git a/src/lib/editSymbol.svelte b/src/lib/editSymbol.svelte new file mode 100644 index 0000000..554543e --- /dev/null +++ b/src/lib/editSymbol.svelte @@ -0,0 +1,38 @@ + + +{#snippet EditSymbol(onClick)} + + + + + + + + +{/snippet} diff --git a/src/lib/loadingModal.svelte b/src/lib/loadingModal.svelte new file mode 100644 index 0000000..f786e4a --- /dev/null +++ b/src/lib/loadingModal.svelte @@ -0,0 +1,13 @@ + + +{#snippet loadingModal()} + + + +{/snippet} diff --git a/src/lib/settingsSymbol.svelte b/src/lib/settingsSymbol.svelte new file mode 100644 index 0000000..ea4933c --- /dev/null +++ b/src/lib/settingsSymbol.svelte @@ -0,0 +1,51 @@ + + +{#snippet settingsSymbol()} + + + + + + + + +{/snippet} diff --git a/src/lib/simplefin.js b/src/lib/simplefin.js index cb63754..13ea8d6 100644 --- a/src/lib/simplefin.js +++ b/src/lib/simplefin.js @@ -1,14 +1,17 @@ +const url = + 'https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin'; -export async function fetchAccounts(url, startDate) { - const { username, password, origin, pathname } = new URL(url); - const start = Math.floor(startDate.getTime() / 1000); - const apiUrl = `${origin}${pathname}/accounts?start-date=${start}`; - const headers = {}; +export async function fetchAccounts(startDate) { + const { username, password, origin, pathname } = new URL(url); + const apiUrl = `${origin}${pathname}/accounts?start-date=${startDate}`; + const headers = {}; - if (username && password) { - headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`); - } + console.log(`Fetching accounts from: ${apiUrl}`); - const response = await fetch(apiUrl, { headers }); - return await response.json(); -} \ No newline at end of file + if (username && password) { + headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`); + } + + const response = await fetch(apiUrl, { headers }); + return await response.json(); +} diff --git a/src/lib/trashbin.svelte b/src/lib/trashbin.svelte new file mode 100644 index 0000000..93b7535 --- /dev/null +++ b/src/lib/trashbin.svelte @@ -0,0 +1,18 @@ + + +{#snippet TrashBin()} + + + +{/snippet} diff --git a/src/routes/+layout.server.js b/src/routes/+layout.server.js index a2c8a68..37f476c 100644 --- a/src/routes/+layout.server.js +++ b/src/routes/+layout.server.js @@ -1,9 +1,10 @@ -import { getAccounts, getBudgets } from "$lib/db"; -import { get } from "svelte/store"; +import { getAccounts, getBudgets, getTotal } from '$lib/db'; +import { get } from 'svelte/store'; export async function load({ params }) { let accounts = await getAccounts(); let budgets = await getBudgets(); + let total = await getTotal(); - return { accounts, budgets }; -} \ No newline at end of file + return { accounts, budgets, total }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f1829b8..aa5f914 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,72 +2,55 @@ import '../app.css'; let { children, data } = $props(); let budgets = $derived(data.budgets); - let newBudget = $state({ - name: '', - amount: 0, - notes: '' - }); - async function saveBudget() { - let res = await fetch('/budget', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(newBudget) - }); - if (res.ok) { - AddBudgetModal.close(); - budgets.push({ - id: res.text(), // Temporary ID, replace with actual ID from the server - ...newBudget - }); - newBudget = { name: '', amount: 0, notes: '' }; // Reset the form - // Optionally, you can refresh the budgets list or show a success message - } else { - console.error('Failed to save budget'); - } - } + let total = $derived(data.total);
- + {@render children()} -
- - - - diff --git a/src/routes/account/[slug]/+page.server.js b/src/routes/account/[slug]/+page.server.js index ef77465..78649db 100644 --- a/src/routes/account/[slug]/+page.server.js +++ b/src/routes/account/[slug]/+page.server.js @@ -1,15 +1,17 @@ import { error } from '@sveltejs/kit'; -import { getAccount, getTransactions } from '$lib/db'; +import { getAccount, getTransactions, getBudgets, getBudgetTransactionsForAccount } from '$lib/db'; /** @type {import('./$types').PageServerLoad} */ export async function load({ params }) { - const transactions = await getTransactions(params.slug); - const account = await getAccount(params.slug); - const slug = params.slug; + const slug = params.slug; + const transactions = await getTransactions(slug); + const account = await getAccount(slug); + const budgets = await getBudgets(); + const budgetTransactions = await getBudgetTransactionsForAccount(slug); + if (transactions) { - return {transactions, account}; + return { transactions, account, budgets, slug, budgetTransactions }; } error(404, 'Not found'); } - diff --git a/src/routes/account/[slug]/+page.svelte b/src/routes/account/[slug]/+page.svelte index 48f6179..11a82f6 100644 --- a/src/routes/account/[slug]/+page.svelte +++ b/src/routes/account/[slug]/+page.svelte @@ -1,10 +1,13 @@
-

{account?.name}

-
{account?.balance}
-
- settings_modal.showModal()} fill="#000000" height="20px" width="20px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - viewBox="0 0 478.703 478.703" xml:space="preserve"> - - - - - - - -
+
+

{account?.name}

+
+
{account?.balance}
+
+ +

Transcations

@@ -94,18 +70,55 @@ Date + Payee Description Amount Notes + Budgets + {#each trans as transaction} - editNotes(transaction)}> + {@const applicableBudgets = budgetTransactions.filter( + (bt) => bt.transaction_id === transaction.id + )} + {@const budgetTotal = applicableBudgets.reduce( + (accumulator, currentValue) => accumulator + Number(currentValue.amount), + 0 + )} + {transaction.date.toDateString()} + {transaction.payee ?? ''} {transaction.description} - {transaction.amount} + {transaction.notes} + + + {/each} @@ -116,14 +129,66 @@
-

{currentTransaction?.description}

-

${currentTransaction?.amount}

-

{currentTransaction?.date?.toDateString()}

-
+
+

{currentTransaction?.description}

+

${currentTransaction?.amount}

+

{currentTransaction?.date?.toDateString()}

+ Notes -
- + + + + Add to budget + + Amount + + Notes + +

Must a sensible number

+ + + @@ -138,11 +203,14 @@ Hide - diff --git a/src/routes/api/budget/[slug]/+server.js b/src/routes/api/budget/[slug]/+server.js index 3c8d6f5..ac8e628 100644 --- a/src/routes/api/budget/[slug]/+server.js +++ b/src/routes/api/budget/[slug]/+server.js @@ -1,4 +1,4 @@ -import { deleteBudget } from '$lib/db.js'; +import { deleteBudget, restoreBudget } from '$lib/db.js'; export function DELETE({ params }) { const { slug } = params; @@ -8,4 +8,12 @@ export function DELETE({ params }) { return deleteBudget(slug) .then(() => new Response(`Budget with slug ${slug} deleted successfully.`)) .catch(err => new Response(`Error deleting budget: ${err.message}`, { status: 500 })); +} + +export async function PUT({ params, request }) { + const { slug } = params; + + restoreBudget(slug) + .then(() => new Response(`Budget with slug ${slug} restored successfully.`)) + .catch(err => new Response(`Error restoring budget: ${err.message}`, { status: 500 })); } \ No newline at end of file diff --git a/src/routes/api/budget/[slug]/transaction/+server.js b/src/routes/api/budget/[slug]/transaction/+server.js new file mode 100644 index 0000000..8c608ff --- /dev/null +++ b/src/routes/api/budget/[slug]/transaction/+server.js @@ -0,0 +1,46 @@ +import { addBudgetTransaction, updateBudgetTransaction, deleteBudgetTransaction } from '$lib/db.js'; + +export async function POST({ params, request }) { + const { slug } = params; + let body = await request.json(); + + const { transactionId, amount, notes } = body; + console.log({ slug, transactionId, amount }); + + // Call the deleteBudget function from db.js (budgetId, transactionId, amount) + return addBudgetTransaction(slug, transactionId, amount, notes) + .then(() => new Response(`Budget transaction added successfully`, { status: 200 })) + .catch( + (err) => new Response(`Error adding transaction to budget ${err.message}`, { status: 500 }) + ); +} + +export async function PATCH({ params, request }) { + const { slug } = params; + let body = await request.json(); + + const { amount, notes } = body; + console.log({ slug, transactionId, amount }); + + // Call the deleteBudget function from db.js (budgetId, transactionId, amount) + return updateBudgetTransaction(slug, amount, notes) + .then(() => new Response(`Budget transaction updated successfully`, { status: 200 })) + .catch( + (err) => new Response(`Error updating transaction in budget ${err.message}`, { status: 500 }) + ); +} + +export async function DELETE({ params, request }) { + const { slug } = params; + + const { transactionId } = slug; + console.log({ slug }); + + // Call the deleteBudget function from db.js (budgetId, transactionId) + return deleteBudgetTransaction(slug) + .then(() => new Response(`Budget transaction deleted successfully`, { status: 200 })) + .catch( + (err) => + new Response(`Error deleting transaction from budget ${err.message}`, { status: 500 }) + ); +} diff --git a/src/routes/api/rules/+server.js b/src/routes/api/rules/+server.js new file mode 100644 index 0000000..e32417f --- /dev/null +++ b/src/routes/api/rules/+server.js @@ -0,0 +1,19 @@ + +import { addRule, runRules } from '$lib/db.js'; + +export async function PUT({ params, request }) { + const { name, description, payee, amount, transdescription, action, actionAmount, account_id, amount_is_precent, priority, use_priority} = await request.json(); + + return addRule(name, description, payee, amount, transdescription, action, actionAmount, account_id, amount_is_precent, priority, use_priority) + .then(() => new Response(`Rule ${name} created successfully.`)) + .catch(err => new Response(`Error creating rule: ${err.message}`, { status: 500 })); +} + + +export async function POST({ params, request }) { + + return runRules() + .then(() => new Response(`Rules executed successfully.`)) +} + + diff --git a/src/routes/api/rules/[slug]/+server.js b/src/routes/api/rules/[slug]/+server.js new file mode 100644 index 0000000..1a2e2f1 --- /dev/null +++ b/src/routes/api/rules/[slug]/+server.js @@ -0,0 +1,21 @@ + + +import { updateRule, deleteRule } from '$lib/db.js'; + +export async function PATCH({ params, request }) { + const { slug } = params; + const { name, description, payee, amount, transdescription, action, actionAmount, account_id, amount_is_precent, priority, use_priority} = await request.json(); + console.log(`Updating rule for with slug: ${slug}`); + + return updateRule(name, description, payee, amount, transdescription, action, actionAmount, account_id, amount_is_precent, priority, use_priority, slug) + .then(() => new Response(`Rule with slug ${slug} updated successfully.`)) + .catch(err => new Response(`Error updating rule: ${err.message}`, { status: 500 })); +} + +export async function DELETE({ params }) { + const { slug } = params; + + return deleteRule(slug) + .then(() => new Response(`Deleted rule with slug ${slug}.`)) + .catch(err => new Response(`Error deleting rule: ${err.message}`, { status: 500 })); +} diff --git a/src/routes/api/simplefin/update/+server.js b/src/routes/api/simplefin/update/+server.js index d2abd02..a40e4dd 100644 --- a/src/routes/api/simplefin/update/+server.js +++ b/src/routes/api/simplefin/update/+server.js @@ -1,10 +1,17 @@ - import { fetchAccounts } from '$lib/simplefin'; -import { updateAccounts } from '$lib/db' ; +import { runRules, updateAccounts } from '$lib/db'; -const url = "https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin"; +export async function POST({ request }) { + let body = null; + try { + let body = await request.json(); + } catch (error) {} + const startDate = body?.startDate + ? body + : Math.floor(new Date(new Date().getFullYear(), new Date().getMonth() - 1).getTime() / 1000); -export async function POST() { - const res = await fetchAccounts(url, new Date("2025-07-03")) - return new Response(await updateAccounts(res)); -} \ No newline at end of file + const res = await fetchAccounts(startDate); + await updateAccounts(res); + await runRules(); + return new Response(`Accounts updated successfully`, { status: 200 }); +} diff --git a/src/routes/budget/[slug]/+page.server.js b/src/routes/budget/[slug]/+page.server.js index 915b457..9e40180 100644 --- a/src/routes/budget/[slug]/+page.server.js +++ b/src/routes/budget/[slug]/+page.server.js @@ -1,12 +1,13 @@ import { error } from '@sveltejs/kit'; -import { getBudgetTransactions } from '$lib/db.js'; +import { getBudget, getBudgetTransactions } from '$lib/db.js'; export async function load({ params }) { const { slug } = params; console.log(`Loading transactions for budget: ${slug}`); const transactions = await getBudgetTransactions(slug); + const budget = await getBudget(slug); - return { transactions, slug }; + return { transactions, slug, budget }; } diff --git a/src/routes/budget/[slug]/+page.svelte b/src/routes/budget/[slug]/+page.svelte index 19b9203..b1b473c 100644 --- a/src/routes/budget/[slug]/+page.svelte +++ b/src/routes/budget/[slug]/+page.svelte @@ -1,91 +1,193 @@ +{#if budget.delete} + +{/if} +
-

{budget.name}

-
{budget.amount}
-
- DeleteBudgetModal.showModal()} - > - - -
+

{budget.name}

+
{budget.amount}
- -
-

Notes: {budget.notes}

+
+

Notes:

+

{budget.notes}

- - - - - - - - - - {#each transactions as txn} - - - - - - {/each} - {#if transactions.length === 0} - - - - {/if} - +
DateDescriptionAmount
{txn.date}{txn.description}${txn.amount}
No transactions found.
+ + + + + + + + + + + {#each transactions as txn} + + + + + + + + {/each} + {#if transactions.length === 0} + + + + {/if} +
DateDescriptionAmountNotes
{txn.date.toDateString()}{txn.description}${txn.budget_amount} + {#if txn.notes} + {txn.notes} + {:else} + No notes + {/if} +
No transactions found.
- - + + + + + + + + + + + + diff --git a/src/routes/rules/+page.server.js b/src/routes/rules/+page.server.js new file mode 100644 index 0000000..a89898a --- /dev/null +++ b/src/routes/rules/+page.server.js @@ -0,0 +1,10 @@ +import { error } from '@sveltejs/kit'; +import { getRules } from '$lib/db.js'; + +export async function load({ params }) { + const rules = await getRules(); + return { rules }; +} + + + diff --git a/src/routes/rules/+page.svelte b/src/routes/rules/+page.svelte new file mode 100644 index 0000000..12c33d3 --- /dev/null +++ b/src/routes/rules/+page.svelte @@ -0,0 +1,318 @@ + + +{#if loading} + {@render loadingModal()} +{/if} + +
+ + + + + + + + + + + + + + + + + + + + {#each rules as rule} + + + + + + + + + + + + + + + {/each} + + + +
PriorityUse PriorityNameDescriptionPayeeAmountTransaction DescriptionAccountAction AmountPrecent?Budget
{rule.priority} + {`${rule.use_priority ? 'Yes' : 'No'}`} + {rule.name}{rule.description}{rule.payee}{rule.amount}{rule.transdescription}{accounts?.find((e) => e.id == rule?.account_id)?.name}{rule.action_amount}{rule.amount_is_precent ? '%' : '$'}{budgets?.find((e) => e.id == rule?.action)?.name}
+
+ + + + + + + + + + + + diff --git a/src/routes/settings/+page.server.js b/src/routes/settings/+page.server.js index 1ddd4fb..ce15647 100644 --- a/src/routes/settings/+page.server.js +++ b/src/routes/settings/+page.server.js @@ -1,9 +1,11 @@ -import { getHiddenAccounts, getDeletedBudgets } from "$lib/db"; -import { get } from "svelte/store"; +import { getHiddenAccounts, getDeletedBudgets, getBudgets } from "$lib/db"; + export async function load({ params }) { let accounts = await getHiddenAccounts(); - let budgets = await getDeletedBudgets(); + let deletedBudgets = await getDeletedBudgets(); + let budgets = await getBudgets(); - return { accounts, budgets }; + + return { accounts, deletedBudgets, budgets }; } \ No newline at end of file diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 5cfefe7..5a322a9 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,42 +1,153 @@ - +{#if loading} + {@render loadingModal()} +{/if} + +
-

Settings

-
-

Hidden Accounts

- {#if accounts.length > 0} - - {:else} -

No hidden accounts.

- {/if} -
+

Settings

+
+

Hidden Accounts

+ {#if accounts.length > 0} + + {:else} +

No hidden accounts.

+ {/if} +
-
-

Deleted Budgets

- {#if budgets.length > 0} - - {:else} -

No deleted budgets.

- {/if} -
-
\ No newline at end of file +
+

Budgets

+
+ {#if budgets.length > 0} +
    + {#each budgets as budget} +
  • + +
  • + {/each} +
+ {:else} +

No budgets.

+ {/if} +
+
+ +
+

Deleted Budgets

+ {#if deletedBudgets.length > 0} + + {:else} +

No deleted budgets.

+ {/if} +
+
+ +
+ Add a budget + + + + + + + + +
+ + + + + diff --git a/src/routes/settings/deleteBudget.svelte b/src/routes/settings/deleteBudget.svelte new file mode 100644 index 0000000..e073dcc --- /dev/null +++ b/src/routes/settings/deleteBudget.svelte @@ -0,0 +1,23 @@ + + +{#snippet deleteBudgetForm(name, deleteBudget)} +
+ +
+

+ Are you sure you want to delete {name}? type its name in the box below if so. +

+
+ +
+ +{/snippet} diff --git a/src/routes/transcation/[slug]/+server.js b/src/routes/transcation/[slug]/+server.js index 1cfbb4e..8cf21ea 100644 --- a/src/routes/transcation/[slug]/+server.js +++ b/src/routes/transcation/[slug]/+server.js @@ -3,8 +3,7 @@ import {setTransactionNote} from '$lib/db'; export async function POST(request) { let body = await request.request.json(); let res = await setTransactionNote(request.params.slug, body.notes); - console.log({slug: request.params.slug, notes: body.notes}); - console.log(res); + return new Response(JSON.stringify({success: true}), { headers: { 'Content-Type': 'application/json' diff --git a/wipedata.psql b/wipedata.psql new file mode 100644 index 0000000..eb55d41 --- /dev/null +++ b/wipedata.psql @@ -0,0 +1,4 @@ +\c budget budget sql.caseytimm.com +TRUNCATE budget_transaction; +UPDATE transaction +SET processed = false; \ No newline at end of file