kinda there

This commit is contained in:
2025-07-18 23:02:01 -04:00
parent 3e7572f3a3
commit 3225ebb986
28 changed files with 1665 additions and 475 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"vscode-postgres.defaultConnection": "sql.caseytimm.com"
}

162
package-lock.json generated
View File

@ -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"
}
}

View File

@ -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"
}

18
src/hooks.server.js Normal file
View File

@ -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
);

View File

@ -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
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;

38
src/lib/editSymbol.svelte Normal file
View File

@ -0,0 +1,38 @@
<script module>
export { EditSymbol };
</script>
{#snippet EditSymbol(onClick)}
<svg
fill="#000000"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="25px"
height="25px"
viewBox="0 0 494.936 494.936"
xml:space="preserve"
>
<g>
<g>
<path
fill="#FEFEFE"
d="M389.844,182.85c-6.743,0-12.21,5.467-12.21,12.21v222.968c0,23.562-19.174,42.735-42.736,42.735H67.157
c-23.562,0-42.736-19.174-42.736-42.735V150.285c0-23.562,19.174-42.735,42.736-42.735h267.741c6.743,0,12.21-5.467,12.21-12.21
s-5.467-12.21-12.21-12.21H67.157C30.126,83.13,0,113.255,0,150.285v267.743c0,37.029,30.126,67.155,67.157,67.155h267.741
c37.03,0,67.156-30.126,67.156-67.155V195.061C402.054,188.318,396.587,182.85,389.844,182.85z"
/>
<path
fill="#FEFEFE"
d="M483.876,20.791c-14.72-14.72-38.669-14.714-53.377,0L221.352,229.944c-0.28,0.28-3.434,3.559-4.251,5.396l-28.963,65.069
c-2.057,4.619-1.056,10.027,2.521,13.6c2.337,2.336,5.461,3.576,8.639,3.576c1.675,0,3.362-0.346,4.96-1.057l65.07-28.963
c1.83-0.815,5.114-3.97,5.396-4.25L483.876,74.169c7.131-7.131,11.06-16.61,11.06-26.692
C494.936,37.396,491.007,27.915,483.876,20.791z M466.61,56.897L257.457,266.05c-0.035,0.036-0.055,0.078-0.089,0.107
l-33.989,15.131L238.51,247.3c0.03-0.036,0.071-0.055,0.107-0.09L447.765,38.058c5.038-5.039,13.819-5.033,18.846,0.005
c2.518,2.51,3.905,5.855,3.905,9.414C470.516,51.036,469.127,54.38,466.61,56.897z"
/>
</g>
</g>
</svg>
{/snippet}

View File

@ -0,0 +1,13 @@
<script module>
export { loadingModal };
</script>
{#snippet loadingModal()}
<dialog id="my_modal_1" class="modal modal-open">
<div class="modal-box">
<div class="flex items-center justify-center">
<span class="loading loading-spinner loading-xl"></span>
</div>
</div>
</dialog>
{/snippet}

View File

@ -0,0 +1,51 @@
<script module>
export { settingsSymbol };
</script>
{#snippet settingsSymbol()}
<svg
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"
>
<g>
<g>
<path
fill="#FEFEFE"
d="M454.2,189.101l-33.6-5.7c-3.5-11.3-8-22.2-13.5-32.6l19.8-27.7c8.4-11.8,7.1-27.9-3.2-38.1l-29.8-29.8
c-5.6-5.6-13-8.7-20.9-8.7c-6.2,0-12.1,1.9-17.1,5.5l-27.8,19.8c-10.8-5.7-22.1-10.4-33.8-13.9l-5.6-33.2
c-2.4-14.3-14.7-24.7-29.2-24.7h-42.1c-14.5,0-26.8,10.4-29.2,24.7l-5.8,34c-11.2,3.5-22.1,8.1-32.5,13.7l-27.5-19.8
c-5-3.6-11-5.5-17.2-5.5c-7.9,0-15.4,3.1-20.9,8.7l-29.9,29.8c-10.2,10.2-11.6,26.3-3.2,38.1l20,28.1
c-5.5,10.5-9.9,21.4-13.3,32.7l-33.2,5.6c-14.3,2.4-24.7,14.7-24.7,29.2v42.1c0,14.5,10.4,26.8,24.7,29.2l34,5.8
c3.5,11.2,8.1,22.1,13.7,32.5l-19.7,27.4c-8.4,11.8-7.1,27.9,3.2,38.1l29.8,29.8c5.6,5.6,13,8.7,20.9,8.7c6.2,0,12.1-1.9,17.1-5.5
l28.1-20c10.1,5.3,20.7,9.6,31.6,13l5.6,33.6c2.4,14.3,14.7,24.7,29.2,24.7h42.2c14.5,0,26.8-10.4,29.2-24.7l5.7-33.6
c11.3-3.5,22.2-8,32.6-13.5l27.7,19.8c5,3.6,11,5.5,17.2,5.5l0,0c7.9,0,15.3-3.1,20.9-8.7l29.8-29.8c10.2-10.2,11.6-26.3,3.2-38.1
l-19.8-27.8c5.5-10.5,10.1-21.4,13.5-32.6l33.6-5.6c14.3-2.4,24.7-14.7,24.7-29.2v-42.1
C478.9,203.801,468.5,191.501,454.2,189.101z M451.9,260.401c0,1.3-0.9,2.4-2.2,2.6l-42,7c-5.3,0.9-9.5,4.8-10.8,9.9
c-3.8,14.7-9.6,28.8-17.4,41.9c-2.7,4.6-2.5,10.3,0.6,14.7l24.7,34.8c0.7,1,0.6,2.5-0.3,3.4l-29.8,29.8c-0.7,0.7-1.4,0.8-1.9,0.8
c-0.6,0-1.1-0.2-1.5-0.5l-34.7-24.7c-4.3-3.1-10.1-3.3-14.7-0.6c-13.1,7.8-27.2,13.6-41.9,17.4c-5.2,1.3-9.1,5.6-9.9,10.8l-7.1,42
c-0.2,1.3-1.3,2.2-2.6,2.2h-42.1c-1.3,0-2.4-0.9-2.6-2.2l-7-42c-0.9-5.3-4.8-9.5-9.9-10.8c-14.3-3.7-28.1-9.4-41-16.8
c-2.1-1.2-4.5-1.8-6.8-1.8c-2.7,0-5.5,0.8-7.8,2.5l-35,24.9c-0.5,0.3-1,0.5-1.5,0.5c-0.4,0-1.2-0.1-1.9-0.8l-29.8-29.8
c-0.9-0.9-1-2.3-0.3-3.4l24.6-34.5c3.1-4.4,3.3-10.2,0.6-14.8c-7.8-13-13.8-27.1-17.6-41.8c-1.4-5.1-5.6-9-10.8-9.9l-42.3-7.2
c-1.3-0.2-2.2-1.3-2.2-2.6v-42.1c0-1.3,0.9-2.4,2.2-2.6l41.7-7c5.3-0.9,9.6-4.8,10.9-10c3.7-14.7,9.4-28.9,17.1-42
c2.7-4.6,2.4-10.3-0.7-14.6l-24.9-35c-0.7-1-0.6-2.5,0.3-3.4l29.8-29.8c0.7-0.7,1.4-0.8,1.9-0.8c0.6,0,1.1,0.2,1.5,0.5l34.5,24.6
c4.4,3.1,10.2,3.3,14.8,0.6c13-7.8,27.1-13.8,41.8-17.6c5.1-1.4,9-5.6,9.9-10.8l7.2-42.3c0.2-1.3,1.3-2.2,2.6-2.2h42.1
c1.3,0,2.4,0.9,2.6,2.2l7,41.7c0.9,5.3,4.8,9.6,10,10.9c15.1,3.8,29.5,9.7,42.9,17.6c4.6,2.7,10.3,2.5,14.7-0.6l34.5-24.8
c0.5-0.3,1-0.5,1.5-0.5c0.4,0,1.2,0.1,1.9,0.8l29.8,29.8c0.9,0.9,1,2.3,0.3,3.4l-24.7,34.7c-3.1,4.3-3.3,10.1-0.6,14.7
c7.8,13.1,13.6,27.2,17.4,41.9c1.3,5.2,5.6,9.1,10.8,9.9l42,7.1c1.3,0.2,2.2,1.3,2.2,2.6v42.1H451.9z"
/>
<path
fill="#FEFEFE"
d="M239.4,136.001c-57,0-103.3,46.3-103.3,103.3s46.3,103.3,103.3,103.3s103.3-46.3,103.3-103.3S296.4,136.001,239.4,136.001
z M239.4,315.601c-42.1,0-76.3-34.2-76.3-76.3s34.2-76.3,76.3-76.3s76.3,34.2,76.3,76.3S281.5,315.601,239.4,315.601z"
/>
</g>
</g>
</svg>
{/snippet}

View File

@ -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();
}
if (username && password) {
headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`);
}
const response = await fetch(apiUrl, { headers });
return await response.json();
}

18
src/lib/trashbin.svelte Normal file
View File

@ -0,0 +1,18 @@
<script module>
export { TrashBin };
</script>
{#snippet TrashBin()}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="30px"
height="30px"
class="cursor-pointer hover:scale-110 transition"
>
<path
fill="#FEFEFE"
d="M 14.984375 2.4863281 A 1.0001 1.0001 0 0 0 14 3.5 L 14 4 L 8.5 4 A 1.0001 1.0001 0 0 0 7.4863281 5 L 6 5 A 1.0001 1.0001 0 1 0 6 7 L 24 7 A 1.0001 1.0001 0 1 0 24 5 L 22.513672 5 A 1.0001 1.0001 0 0 0 21.5 4 L 16 4 L 16 3.5 A 1.0001 1.0001 0 0 0 14.984375 2.4863281 z M 6 9 L 7.7929688 24.234375 C 7.9109687 25.241375 8.7633438 26 9.7773438 26 L 20.222656 26 C 21.236656 26 22.088031 25.241375 22.207031 24.234375 L 24 9 L 6 9 z"
/>
</svg>
{/snippet}

View File

@ -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 };
}
return { accounts, budgets, total };
}

View File

@ -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);
</script>
<div class="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col m-5">
<!-- Page content here -->
<div class="navbar bg-base-100 shadow-sm">
<div class="flex-none">
<label
for="my-drawer-2"
class="btn btn-primary drawer-button lg:hidden btn-square btn-ghost"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-5 w-5 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</label>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl">Timm Budget</a>
</div>
</div>
{@render children()}
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden"> Open drawer </label>
</div>
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<li>
<a href="/">
<span class="text-lg font-bold">Total: {total}</span>
</a>
</li>
<li><div class="divider">Budgets</div></li>
{#each budgets as budget}
<li>
<a href={`/budget/${budget.id}`}>
{budget.name} ({budget.amount})
{budget.name} ({budget.sum})
</a>
</li>
{/each}
<li>
<button class="btn btn-circle" aria-label="Add" onclick={() => AddBudgetModal.showModal()}>
<svg
fill="#F0F0F0F0"
height="800px"
width="800px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 500 500"
enable-background="new 0 0 500 500"
xml:space="preserve"
>
<path
d="M306,192h-48v-48c0-4.4-3.6-8-8-8s-8,3.6-8,8v48h-48c-4.4,0-8,3.6-8,8s3.6,8,8,8h48v48c0,4.4,3.6,8,8,8s8-3.6,8-8v-48h48
c4.4,0,8-3.6,8-8S310.4,192,306,192z"
/>
</svg>
</button>
</li>
<li><div class="divider">Accounts</div></li>
{#each data.accounts as account}
<li>
@ -77,31 +60,8 @@
</li>
{/each}
<li><div class="divider"></div></li>
<li><a href="/settings">Settings</a></li>
<li><a href="/rules">Rules</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</div>
</div>
<dialog id="AddBudgetModal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<div>
<input
bind:value={newBudget.name}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"
/>
<input
bind:value={newBudget.amount}
type="number"
placeholder="Budget Amount"
class="input input-bordered w-full max-w-xs mt-2"
/>
<textarea bind:value={newBudget.notes} class="textarea w-100"></textarea>
</div>
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
</div>
</dialog>

View File

@ -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');
}

View File

@ -1,10 +1,13 @@
<script>
import { EditSymbol } from '$lib/editSymbol.svelte';
import { settingsSymbol } from '$lib/settingsSymbol.svelte';
let { data } = $props();
console.log(data);
let trans = $derived(data.transactions);
let budgets = $derived(data.budgets);
let budgetTransactions = $derived(data.budgetTransactions);
let notes = $state('');
let currentTransaction = $state(null);
let account = $derived(data.account);
let currentTransaction = $state({ budget_id: null, amount: 0, notes: '' });
let account = $derived(data.account);
let hide = $derived(account?.hide || false);
let inTotal = $derived(account?.in_total || false);
@ -32,61 +35,34 @@
console.error('Failed to save notes');
}
}
async function saveSettings() {
let res = await fetch(`/api/account/${account.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
hide: $state.snapshot(hide),
in_total: $state.snapshot(inTotal)
})
});
if (res.ok) {
settings_modal.close();
// Optionally, you can refresh the account data or show a success message
} else {
console.error('Failed to save settings');
}
}
async function saveSettings() {
let res = await fetch(`/api/account/${account.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
hide: $state.snapshot(hide),
in_total: $state.snapshot(inTotal)
})
});
if (res.ok) {
settings_modal.close();
// Optionally, you can refresh the account data or show a success message
} else {
console.error('Failed to save settings');
}
}
</script>
<div class="flex mb-4">
<div class="w-128 flex-none justify-bottom"><h1 class="text-xl font-bold">{account?.name}</h1></div>
<div class="w-64 grow">{account?.balance}</div>
<div class="w-14 flex-none text-right">
<svg onclick={()=>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">
<g>
<g>
<path fill="#FEFEFE" d="M454.2,189.101l-33.6-5.7c-3.5-11.3-8-22.2-13.5-32.6l19.8-27.7c8.4-11.8,7.1-27.9-3.2-38.1l-29.8-29.8
c-5.6-5.6-13-8.7-20.9-8.7c-6.2,0-12.1,1.9-17.1,5.5l-27.8,19.8c-10.8-5.7-22.1-10.4-33.8-13.9l-5.6-33.2
c-2.4-14.3-14.7-24.7-29.2-24.7h-42.1c-14.5,0-26.8,10.4-29.2,24.7l-5.8,34c-11.2,3.5-22.1,8.1-32.5,13.7l-27.5-19.8
c-5-3.6-11-5.5-17.2-5.5c-7.9,0-15.4,3.1-20.9,8.7l-29.9,29.8c-10.2,10.2-11.6,26.3-3.2,38.1l20,28.1
c-5.5,10.5-9.9,21.4-13.3,32.7l-33.2,5.6c-14.3,2.4-24.7,14.7-24.7,29.2v42.1c0,14.5,10.4,26.8,24.7,29.2l34,5.8
c3.5,11.2,8.1,22.1,13.7,32.5l-19.7,27.4c-8.4,11.8-7.1,27.9,3.2,38.1l29.8,29.8c5.6,5.6,13,8.7,20.9,8.7c6.2,0,12.1-1.9,17.1-5.5
l28.1-20c10.1,5.3,20.7,9.6,31.6,13l5.6,33.6c2.4,14.3,14.7,24.7,29.2,24.7h42.2c14.5,0,26.8-10.4,29.2-24.7l5.7-33.6
c11.3-3.5,22.2-8,32.6-13.5l27.7,19.8c5,3.6,11,5.5,17.2,5.5l0,0c7.9,0,15.3-3.1,20.9-8.7l29.8-29.8c10.2-10.2,11.6-26.3,3.2-38.1
l-19.8-27.8c5.5-10.5,10.1-21.4,13.5-32.6l33.6-5.6c14.3-2.4,24.7-14.7,24.7-29.2v-42.1
C478.9,203.801,468.5,191.501,454.2,189.101z M451.9,260.401c0,1.3-0.9,2.4-2.2,2.6l-42,7c-5.3,0.9-9.5,4.8-10.8,9.9
c-3.8,14.7-9.6,28.8-17.4,41.9c-2.7,4.6-2.5,10.3,0.6,14.7l24.7,34.8c0.7,1,0.6,2.5-0.3,3.4l-29.8,29.8c-0.7,0.7-1.4,0.8-1.9,0.8
c-0.6,0-1.1-0.2-1.5-0.5l-34.7-24.7c-4.3-3.1-10.1-3.3-14.7-0.6c-13.1,7.8-27.2,13.6-41.9,17.4c-5.2,1.3-9.1,5.6-9.9,10.8l-7.1,42
c-0.2,1.3-1.3,2.2-2.6,2.2h-42.1c-1.3,0-2.4-0.9-2.6-2.2l-7-42c-0.9-5.3-4.8-9.5-9.9-10.8c-14.3-3.7-28.1-9.4-41-16.8
c-2.1-1.2-4.5-1.8-6.8-1.8c-2.7,0-5.5,0.8-7.8,2.5l-35,24.9c-0.5,0.3-1,0.5-1.5,0.5c-0.4,0-1.2-0.1-1.9-0.8l-29.8-29.8
c-0.9-0.9-1-2.3-0.3-3.4l24.6-34.5c3.1-4.4,3.3-10.2,0.6-14.8c-7.8-13-13.8-27.1-17.6-41.8c-1.4-5.1-5.6-9-10.8-9.9l-42.3-7.2
c-1.3-0.2-2.2-1.3-2.2-2.6v-42.1c0-1.3,0.9-2.4,2.2-2.6l41.7-7c5.3-0.9,9.6-4.8,10.9-10c3.7-14.7,9.4-28.9,17.1-42
c2.7-4.6,2.4-10.3-0.7-14.6l-24.9-35c-0.7-1-0.6-2.5,0.3-3.4l29.8-29.8c0.7-0.7,1.4-0.8,1.9-0.8c0.6,0,1.1,0.2,1.5,0.5l34.5,24.6
c4.4,3.1,10.2,3.3,14.8,0.6c13-7.8,27.1-13.8,41.8-17.6c5.1-1.4,9-5.6,9.9-10.8l7.2-42.3c0.2-1.3,1.3-2.2,2.6-2.2h42.1
c1.3,0,2.4,0.9,2.6,2.2l7,41.7c0.9,5.3,4.8,9.6,10,10.9c15.1,3.8,29.5,9.7,42.9,17.6c4.6,2.7,10.3,2.5,14.7-0.6l34.5-24.8
c0.5-0.3,1-0.5,1.5-0.5c0.4,0,1.2,0.1,1.9,0.8l29.8,29.8c0.9,0.9,1,2.3,0.3,3.4l-24.7,34.7c-3.1,4.3-3.3,10.1-0.6,14.7
c7.8,13.1,13.6,27.2,17.4,41.9c1.3,5.2,5.6,9.1,10.8,9.9l42,7.1c1.3,0.2,2.2,1.3,2.2,2.6v42.1H451.9z"/>
<path fill="#FEFEFE" d="M239.4,136.001c-57,0-103.3,46.3-103.3,103.3s46.3,103.3,103.3,103.3s103.3-46.3,103.3-103.3S296.4,136.001,239.4,136.001
z M239.4,315.601c-42.1,0-76.3-34.2-76.3-76.3s34.2-76.3,76.3-76.3s76.3,34.2,76.3,76.3S281.5,315.601,239.4,315.601z"/>
</g>
</g>
</svg>
</div>
<div class="w-128 flex-none justify-bottom">
<h1 class="text-xl font-bold">{account?.name}</h1>
</div>
<div class="w-64 grow">{account?.balance}</div>
<div class="w-14 flex-none text-right">
<button class="btn btn-square btn-ghost">{@render settingsSymbol()} </button>
</div>
</div>
<h1>Transcations</h1>
@ -94,18 +70,55 @@
<thead>
<tr>
<th>Date</th>
<th>Payee</th>
<th>Description</th>
<th>Amount</th>
<th>Notes</th>
<th>Budgets</th>
<th></th>
</tr>
</thead>
<tbody>
{#each trans as transaction}
<tr class="hover:bg-base-300" onclick={() => editNotes(transaction)}>
{@const applicableBudgets = budgetTransactions.filter(
(bt) => bt.transaction_id === transaction.id
)}
{@const budgetTotal = applicableBudgets.reduce(
(accumulator, currentValue) => accumulator + Number(currentValue.amount),
0
)}
<tr class="hover:bg-base-300">
<td>{transaction.date.toDateString()}</td>
<td>{transaction.payee ?? ''}</td>
<td>{transaction.description}</td>
<td>{transaction.amount}</td>
<td
><ul class="list bg-base-100 rounded-box shadow-md">
<li class="list-row">Amount: {transaction.amount}</li>
<li class="list-row">Budget: {budgetTotal.toFixed(2)}</li>
<li class="list-row">Remains: {(transaction.amount - budgetTotal).toFixed(2)}</li>
</ul></td
>
<td>{transaction.notes}</td>
<td>
<ul class="list bg-base-100 rounded-box shadow-md">
{#each applicableBudgets as budgetTransaction}
<li class="list-row">
<div class="flex">
<div class="flex-auto w-24">
{budgets.find((b) => b.id === budgetTransaction.budget_id)?.name}
</div>
<div class="flex-auto w-16">${budgetTransaction.amount}</div>
<div class="flex-auto w-48">{budgetTransaction.notes}</div>
</div>
</li>
{/each}
</ul></td
>
<td
><button class="btn btn-square btn-ghost" onclick={() => editNotes(transaction)}
>{@render EditSymbol()}</button
></td
>
</tr>
{/each}
</tbody>
@ -116,14 +129,66 @@
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h3>{currentTransaction?.description}</h3>
<h4>${currentTransaction?.amount}</h4>
<p>{currentTransaction?.date?.toDateString()}</p>
<div>
<fieldset class="fieldset">
<p class="label">{currentTransaction?.description}</p>
<p class="label">${currentTransaction?.amount}</p>
<p class="label">{currentTransaction?.date?.toDateString()}</p>
<legend class="fieldset-legend">Notes</legend>
<textarea bind:value={notes} class="textarea w-100"></textarea>
</div>
<button onclick={() => saveNotes()} class="btn btn-primary mt-4">Save</button>
<button class="btn btn-neutral" onclick={() => saveNotes()}>Save</button>
<legend class="fieldset-legend">Add to budget</legend>
<select bind:value={currentTransaction.budget_id} class="select">
<option disabled selected>Pick a budget</option>
{#each budgets as budget}
<option value={budget.id}>{budget.name} - {budget.sum}</option>
{/each}
</select>
<legend class="fieldset-legend">Amount</legend>
<input
bind:value={currentTransaction.amount}
type="number"
class="input validator"
required
placeholder="Amount"
title="Amount"
/>
<legend class="fieldset-legend">Notes</legend>
<textarea bind:value={currentTransaction.notes} class="textarea w-100"></textarea>
<p class="validator-hint">Must a sensible number</p>
<button
class="btn btn-primary"
onclick={() => {
if (budget_id) {
fetch(`/api/budget/${budget_id}/transaction`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
transactionId: currentTransaction.id,
amount: currentTransaction.amount,
notes: currentTransaction.notes
})
}).then((res) => {
if (res.ok) {
// Optionally, you can refresh the UI or show a success message
console.log('Transaction added to budget successfully');
} else {
console.error('Failed to add transaction to budget');
}
});
} else {
console.error('No budget selected');
}
}}>Add to Budget</button
>
</fieldset>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog id="settings_modal" class="modal">
@ -138,11 +203,14 @@
<input type="checkbox" bind:checked={hide} class="toggle" />
Hide
</label>
<label class="label">
<label class="label">
<input type="checkbox" bind:checked={inTotal} class="toggle" />
Use in total
</label>
</fieldset>
<button onclick={() => saveSettings()} class="btn btn-primary mt-4">Save</button>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -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 }));
}

View File

@ -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 })
);
}

View File

@ -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.`))
}

View File

@ -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 }));
}

View File

@ -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));
}
const res = await fetchAccounts(startDate);
await updateAccounts(res);
await runRules();
return new Response(`Accounts updated successfully`, { status: 200 });
}

View File

@ -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 };
}

View File

@ -1,91 +1,193 @@
<script>
import { EditSymbol } from '$lib/editSymbol.svelte';
import { TrashBin } from '$lib/trashbin.svelte';
let { data } = $props();
let budget = $derived(data.budgets.find((b) => b.id == data.slug) || {});
let transactions = $derived(data.transactions || []);
let toDeleteBudget = $state(null);
let { data } = $props();
let budget = $derived(data.budget);
let transactions = $derived(data.transactions.transactions || []);
let newData = $state({
amount: 0,
notes: ''
});
let toDelete = $state(null);
let toDeleteName = $state('');
async function deleteBudget() {
if (toDeleteBudget === budget.name) {
let res = await fetch(`/api/budget/${budget.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
window.location.href = '/'; // Redirect to budgets list after deletion
} else {
console.error('Failed to delete budget');
}
} else {
alert('Please type the budget name correctly to confirm deletion.');
}
}
async function saveTransaction() {
let res = await fetch(`/api/budget/${budget.id}/transaction`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: $state.snapshot(newData.amount),
notes: $state.snapshot(newData.notes)
})
});
}
function edit(transaction) {
newData.amount = transaction.amount;
newData.notes = transaction.notes || '';
EditBudgetTransactionModal.showModal();
}
async function restoreBudget() {
let res = await fetch(`/api/budget/${budget.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ delete: false })
});
}
function deleteTransaction(transaction) {
toDelete = transaction;
DeleteTransactionModal.showModal();
}
</script>
{#if budget.delete}
<div role="alert" class="alert alert-error" onclick={() => RestoreModal.showModal()}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>This budget has been deleted</span>
</div>
{/if}
<div class="flex mb-4">
<div class="w-32 flex-none justify-bottom"><h1 class="text-2xl font-bold">{budget.name}</h1></div>
<div class="w-64 grow">{budget.amount}</div>
<div class="w-14 flex-none text-right">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="30px"
height="30px"
class="cursor-pointer hover:scale-110 transition"
onclick={()=> DeleteBudgetModal.showModal()}
>
<path fill="#FEFEFE" d="M 14.984375 2.4863281 A 1.0001 1.0001 0 0 0 14 3.5 L 14 4 L 8.5 4 A 1.0001 1.0001 0 0 0 7.4863281 5 L 6 5 A 1.0001 1.0001 0 1 0 6 7 L 24 7 A 1.0001 1.0001 0 1 0 24 5 L 22.513672 5 A 1.0001 1.0001 0 0 0 21.5 4 L 16 4 L 16 3.5 A 1.0001 1.0001 0 0 0 14.984375 2.4863281 z M 6 9 L 7.7929688 24.234375 C 7.9109687 25.241375 8.7633438 26 9.7773438 26 L 20.222656 26 C 21.236656 26 22.088031 25.241375 22.207031 24.234375 L 24 9 L 6 9 z"/>
</svg>
</div>
<div class="w-32 flex-none justify-bottom"><h1 class="text-2xl font-bold">{budget.name}</h1></div>
<div class="w-64 grow">{budget.amount}</div>
</div>
<div class="mb-4">
<p class="text-sm">Notes: {budget.notes}</p>
<div class="">
<h2>Notes:</h2>
<p>{budget.notes}</p>
</div>
<table class="min-w-full border border-gray-300">
<thead>
<tr class="">
<th class="px-4 py-2 border-b">Date</th>
<th class="px-4 py-2 border-b">Description</th>
<th class="px-4 py-2 border-b">Amount</th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<td class="px-4 py-2 border-b">{txn.date}</td>
<td class="px-4 py-2 border-b">{txn.description}</td>
<td class="px-4 py-2 border-b">${txn.amount}</td>
</tr>
{/each}
{#if transactions.length === 0}
<tr>
<td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td>
</tr>
{/if}
</tbody>
<table class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">
<thead>
<tr class="">
<th>Date</th>
<th>Description</th>
<th>Amount</th>
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<td>{txn.date.toDateString()}</td>
<td>{txn.description}</td>
<td>${txn.budget_amount}</td>
<td>
{#if txn.notes}
<span>{txn.notes}</span>
{:else}
<span class="text-gray-500">No notes</span>
{/if}
</td>
<td
><button class="btn btn-square btn-ghost" onclick={() => edit(txn)}
>{@render EditSymbol()}</button
></td
><td
><button class="btn btn-square btn-ghost" onclick={() => deleteTransaction(txn)}
>{@render TrashBin()}</button
></td
></tr
>
{/each}
{#if transactions.length === 0}
<tr>
<td colspan="3">No transactions found.</td>
</tr>
{/if}
</tbody>
</table>
<dialog id="DeleteBudgetModal" class="modal">
<dialog id="EditBudgetTransactionModal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<p>Are you sure you want to delete <code>{budget.name}</code>? type its name in the box below if so.</p>
<div>
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Edit</legend>
<label class="label">Amount</label>
<input
bind:value={toDeleteBudget}
bind:value={newData.amount}
type="text"
placeholder="Budget Name"
placeholder="Amount"
class="input input-bordered w-full max-w-xs"
/>
</div>
<button onclick={()=>deleteBudget()} class="btn btn-primary mt-4">Delete</button>
<label class="label">Notes</label>
<textarea bind:value={newData.notes} class="textarea w-100" placeholder="Budget Notes"
></textarea>
<button onclick={() => saveTransaction()} class="btn btn-primary mt-4">Save</button>
</fieldset>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog id="RestoreModal" class="modal">
<div class="modal-box">
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Restore?</legend>
<button onclick={() => restoreBudget()} class="btn btn-primary mt-4">Restore</button>
</fieldset>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog id="DeleteTransactionModal" class="modal">
<div class="modal-box">
<h1>Are you sure you want to delete {toDelete?.description}?</h1>
<p>Type it in the box to confirm</p>
<input
bind:value={toDeleteName}
type="text"
placeholder="Type the name to confirm"
class="input input-bordered w-full max-w-xs"
/>
<button
class="btn btn-error mt-4"
onclick={async () => {
if (toDelete.description === toDeleteName) {
let res = await fetch(`/api/budget/${toDelete.budget_transaction_id}/transaction/`, {
method: 'DELETE'
});
if (res.ok) {
console.log('Rule deleted successfully');
DeleteTransactionModal.close();
location.reload();
} else {
console.error('Failed to delete transaction');
}
} else {
console.error('Name does not match');
}
}}>Delete</button
>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -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 };
}

View File

@ -0,0 +1,318 @@
<script>
import { EditSymbol } from '$lib/editSymbol.svelte';
import { TrashBin } from '$lib/trashbin.svelte';
import { loadingModal } from '$lib/loadingModal.svelte';
let { data } = $props();
let rules = $derived(data.rules || []);
let budgets = $derived(data.budgets || []);
let accounts = $derived(data.accounts || []);
let loading = $state(false);
let adding = $state(false);
let toDelete = $state(null);
let toDeleteName = $state('');
const newRule = $state({
id: 0,
name: '',
priority: 0,
description: '',
payee: '',
amount: '',
transdescription: '',
action: '',
account_id: null,
actionAmount: 0,
amount_is_precent: false,
use_priority: true
});
async function doTheThing() {
if (adding) {
await addRule();
} else {
await updateRule();
}
}
function clear() {
newRule.name = '';
newRule.priority = 0;
newRule.description = '';
newRule.payee = '';
newRule.amount = '';
newRule.transdescription = '';
newRule.action = '';
newRule.account_id = null;
newRule.actionAmount = 0;
newRule.amount_is_precent = false;
newRule.use_priority = true;
}
async function addRule() {
let res = await fetch('/api/rules', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newRule)
});
if (res.ok) {
// Optionally, you can refresh the rules list or show a success message
console.log('Rule added successfully');
clear();
AddRuleModal.close();
} else {
console.error('Failed to add rule');
}
}
async function updateRule() {
let res = await fetch(`/api/rules/${newRule.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newRule)
});
if (res.ok) {
console.log('Rule updated successfully');
clear();
AddRuleModal.close();
} else {
console.error('Failed to update rule');
}
}
function editRule(rule) {
adding = false;
newRule.name = rule.name;
newRule.priority = rule.priority;
newRule.description = rule.description;
newRule.payee = rule.payee;
newRule.amount = rule.amount;
newRule.transdescription = rule.transdescription;
newRule.action = rule.action;
newRule.id = rule.id;
newRule.actionAmount = rule.action_amount;
newRule.account_id = rule.account_id;
newRule.amount_is_precent = rule.amount_is_precent;
newRule.use_priority = rule.use_priority;
AddRuleModal.showModal();
}
$inspect(newRule);
function deleteRule(rule) {
toDelete = rule;
DeleteModal.showModal();
}
function runRules() {
loading = true;
fetch('/api/rules', {
method: 'POST'
})
.then((response) => response.json())
.then((data) => {
console.log('Rules executed successfully:', data);
})
.catch((error) => {
console.error('Error executing rules:', error);
})
.finally(() => {
loading = false;
});
}
$inspect(rules);
</script>
{#if loading}
{@render loadingModal()}
{/if}
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Priority</th>
<th>Use Priority</th>
<th>Name</th>
<th>Description</th>
<th>Payee</th>
<th>Amount</th>
<th>Transaction Description</th>
<th>Account</th>
<th>Action Amount</th>
<td>Precent?</td>
<th>Budget</th>
<th></th>
</tr>
</thead>
<tbody>
{#each rules as rule}
<tr>
<td>{rule.priority}</td>
<td>
{`${rule.use_priority ? 'Yes' : 'No'}`}
</td><td>{rule.name}</td>
<td>{rule.description}</td>
<td>{rule.payee}</td>
<td>{rule.amount}</td>
<td>{rule.transdescription}</td>
<td>{accounts?.find((e) => e.id == rule?.account_id)?.name}</td>
<td>{rule.action_amount}</td>
<td>{rule.amount_is_precent ? '%' : '$'}</td>
<td>{budgets?.find((e) => e.id == rule?.action)?.name}</td>
<td
><button class="btn btn-square btn-ghost" onclick={() => editRule(rule)}>
{@render EditSymbol()}</button
></td
>
<td
><button class="btn btn-square btn-ghost" onclick={() => deleteRule(rule)}>
{@render TrashBin()}</button
></td
>
</tr>
{/each}
<tr>
<td colspan="7"
><button
onclick={() => {
adding = true;
clear();
AddRuleModal.showModal();
}}
class="btn btn-neutral">Add</button
></td
></tr
>
</tbody>
</table>
</div>
<button onclick={() => runRules()} class="btn btn-primary">Run Rules</button>
<dialog id="AddRuleModal" class="modal">
<div class="modal-box">
<h1>{`${adding ? 'New' : 'Edit'}`} Rule</h1>
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<label class="label">Name</label>
<input
bind:value={newRule.name}
type="text"
placeholder="Name"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Priority</label>
<input
bind:value={newRule.priority}
type="number"
placeholder="priority"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Description</label>
<textarea bind:value={newRule.description} class="textarea w-100" placeholder="Description"
></textarea>
<label class="label">Payee Filter</label>
<input
bind:value={newRule.payee}
type="text"
placeholder="regex"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Amount</label>
<input
bind:value={newRule.amount}
type="text"
placeholder="regex"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Details Filter</label>
<input
bind:value={newRule.transdescription}
type="text"
placeholder="regex"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Account</label>
<select bind:value={newRule.account_id} class="select">
<option disabled selected>Pick a budget</option>
<option value={null}>None</option>
{#each accounts as account}
<option value={account.id.toString()}>{account.name}</option>
{/each}
</select>
<label class="label">Budget</label>
<select bind:value={newRule.action} class="select">
<option disabled selected>Pick a budget</option>
{#each budgets as budget}
<option value={budget.id.toString()}>{budget.name} - {budget.sum}</option>
{/each}
</select>
<label class="label">Action Amount</label>
<input
bind:value={newRule.actionAmount}
type="text"
placeholder="regex"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">
<input type="checkbox" class="checkbox" bind:checked={newRule.amount_is_precent} />
Precentage?
</label>
<label class="label">
<input type="checkbox" class="checkbox" bind:checked={newRule.use_priority} />
Use Priority?
</label>
<button onclick={() => doTheThing()} class="btn btn-primary mt-4">Save</button>
</fieldset>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<dialog id="DeleteModal" class="modal">
<div class="modal-box">
<h1>Are you sure you want to delete {toDelete?.name}?</h1>
<p>Type it in the box to confirm</p>
<input
bind:value={toDeleteName}
type="text"
placeholder="Type the name to confirm"
class="input input-bordered w-full max-w-xs"
/>
<button
class="btn btn-error mt-4"
onclick={async () => {
if (toDelete.name === toDeleteName) {
let res = await fetch(`/api/rules/${toDelete.id}`, {
method: 'DELETE'
});
if (res.ok) {
console.log('Rule deleted successfully');
DeleteModal.close();
} else {
console.error('Failed to delete rule');
}
} else {
console.error('Name does not match');
}
}}>Delete</button
>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -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 };
}

View File

@ -1,42 +1,153 @@
<script>
let { data } = $props();
let accounts = $derived(data.accounts || []);
let budgets = $derived(data.budgets || []);
import { deleteBudgetForm } from './deleteBudget.svelte';
import { loadingModal } from '$lib/loadingModal.svelte';
let { data } = $props();
let accounts = $derived(data.accounts || []);
let deletedBudgets = $derived(data.deletedBudgets || []);
let budgets = $derived(data.budgets || []);
let toDeleteBudget = $state({});
let loading = $state(false);
let newBudget = $state({
amount: 0,
notes: ''
});
async function saveBudget() {
loading = true;
let res = await fetch('/budget', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newBudget)
});
if (res.ok) {
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
window.location.href = '/settings'; // Redirect to budgets list after deletion
} else {
console.error('Failed to save budget');
}
loading = false;
}
async function update() {
loading = true;
let res = await fetch('/api/simplefin/update', {
method: 'POST'
});
loading = false;
}
async function deleteBudget(name) {
loading = true;
if (name === toDeleteBudget.name) {
let res = await fetch(`/api/budget/${toDeleteBudget.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
window.location.href = '/settings'; // Redirect to budgets list after deletion
} else {
console.error('Failed to delete budget');
}
} else {
alert('Please type the budget name correctly to confirm deletion.');
}
loading = false;
}
</script>
<button onclick={update}>Update</button>
{#if loading}
{@render loadingModal()}
{/if}
<button class="btn btn-primary" onclick={update}>Update</button>
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">Settings</h1>
<div class="mb-4">
<h2 class="text-xl font-semibold">Hidden Accounts</h2>
{#if accounts.length > 0}
<ul>
{#each accounts as account}
<li><a href={`/account/${account.id}`}>{account.name}</a></li>
{/each}
</ul>
{:else}
<p>No hidden accounts.</p>
{/if}
</div>
<h1 class="text-2xl font-bold">Settings</h1>
<div class="mb-4">
<h2 class="text-xl font-semibold">Hidden Accounts</h2>
{#if accounts.length > 0}
<ul>
{#each accounts as account}
<li><a href={`/account/${account.id}`}>{account.name}</a></li>
{/each}
</ul>
{:else}
<p>No hidden accounts.</p>
{/if}
</div>
<div class="mb-4">
<h2 class="text-xl font-semibold">Deleted Budgets</h2>
{#if budgets.length > 0}
<ul>
{#each budgets as budget}
<li><a href={`/budget/${budget.id}`}>{budget.name}</a> </li>
{/each}
</ul>
{:else}
<p>No deleted budgets.</p>
{/if}
</div>
</div>
<div class="mb-4">
<h2 class="text-xl font-semibold">Budgets</h2>
<div>
{#if budgets.length > 0}
<ul>
{#each budgets as budget}
<li>
<button
class="btn btn-error"
onclick={() => {
toDeleteBudget = budget;
DeleteBudgetModal.showModal();
}}
>
{budget.name}
</button>
</li>
{/each}
</ul>
{:else}
<p>No budgets.</p>
{/if}
</div>
</div>
<div class="mb-4">
<h2 class="text-xl font-semibold">Deleted Budgets</h2>
{#if deletedBudgets.length > 0}
<ul>
{#each deletedBudgets as budget}
<li><a href={`/budget/${budget.id}`}>{budget.name}</a></li>
{/each}
</ul>
{:else}
<p>No deleted budgets.</p>
{/if}
</div>
</div>
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Add a budget</legend>
<label class="label">Name</label>
<input
bind:value={newBudget.name}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"
/>
<label class="label">Notes</label>
<textarea bind:value={newBudget.notes} class="textarea w-100" placeholder="Budget Notes"
></textarea>
<button onclick={() => saveBudget()} class="btn btn-primary mt-4">Save</button>
</fieldset>
<dialog id="DeleteBudgetModal" class="modal">
<div class="modal-box">
{@render deleteBudgetForm(toDeleteBudget.name, deleteBudget)}
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>

View File

@ -0,0 +1,23 @@
<script module>
export { deleteBudgetForm };
let toDeleteBudget = $state('');
</script>
{#snippet deleteBudgetForm(name, deleteBudget)}
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<p>
Are you sure you want to delete <code>{name}</code>? type its name in the box below if so.
</p>
<div>
<input
bind:value={toDeleteBudget}
type="text"
placeholder="Budget Name"
class="input input-bordered w-full max-w-xs"
/>
</div>
<button onclick={() => deleteBudget(toDeleteBudget)} class="btn btn-primary mt-4">Delete</button>
{/snippet}

View File

@ -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'

4
wipedata.psql Normal file
View File

@ -0,0 +1,4 @@
\c budget budget sql.caseytimm.com
TRUNCATE budget_transaction;
UPDATE transaction
SET processed = false;