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", "name": "budget",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@auth/sveltekit": "^1.10.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"cron": "^4.3.2",
"postgres": "^3.4.7", "postgres": "^3.4.7",
"tailwindcss": "^4.1.11" "tailwindcss": "^4.1.11"
}, },
@ -37,6 +39,63 @@
"node": ">=6.0.0" "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": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5", "version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -497,11 +556,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@polka/url": {
"version": "1.0.0-next.29", "version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
@ -768,7 +835,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"acorn": "^8.9.0" "acorn": "^8.9.0"
@ -788,7 +854,6 @@
"version": "2.22.2", "version": "2.22.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.2.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.2.tgz",
"integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==", "integrity": "sha512-2MvEpSYabUrsJAoq5qCOBGAlkICjfjunrnLcx3YAk2XV7TvAIhomlKsAgR4H/4uns5rAfYmj7Wet5KRtc8dPIg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
@ -821,7 +886,6 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz",
"integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
@ -843,7 +907,6 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.7" "debug": "^4.3.7"
@ -1139,7 +1202,6 @@
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
@ -1148,11 +1210,16 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "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": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -1165,7 +1232,6 @@
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -1175,7 +1241,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -1194,7 +1259,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -1204,12 +1268,24 @@
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -1237,7 +1313,6 @@
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -1255,7 +1330,6 @@
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -1274,7 +1348,6 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
@ -1334,14 +1407,12 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "1.4.9", "version": "1.4.9",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz",
"integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@ -1385,7 +1456,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
@ -1400,11 +1470,19 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -1642,7 +1720,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.castarray": { "node_modules/lodash.castarray": {
@ -1666,6 +1743,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -1715,7 +1801,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -1725,7 +1810,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -1735,7 +1819,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
@ -1756,6 +1839,15 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1829,6 +1921,25 @@
"url": "https://github.com/sponsors/porsager" "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": { "node_modules/prettier": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
@ -1899,7 +2010,6 @@
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mri": "^1.1.0" "mri": "^1.1.0"
@ -1912,14 +2022,12 @@
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sirv": { "node_modules/sirv": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@polka/url": "^1.0.0-next.24", "@polka/url": "^1.0.0-next.24",
@ -1943,7 +2051,6 @@
"version": "5.34.9", "version": "5.34.9",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.9.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.9.tgz",
"integrity": "sha512-sld35zFpooaSRSj4qw8Vl/cyyK0/sLQq9qhJ7BGZo/Kd0ggYtEnvNYLlzhhoqYsYQzA0hJqkzt3RBO/8KoTZOg==", "integrity": "sha512-sld35zFpooaSRSj4qw8Vl/cyyK0/sLQq9qhJ7BGZo/Kd0ggYtEnvNYLlzhhoqYsYQzA0hJqkzt3RBO/8KoTZOg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
@ -2017,7 +2124,6 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -2108,7 +2214,6 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz",
"integrity": "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==", "integrity": "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"tests/deps/*", "tests/deps/*",
@ -2136,7 +2241,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"dev": true,
"license": "MIT" "license": "MIT"
} }
} }

View File

@ -23,7 +23,9 @@
"vite": "^6.2.6" "vite": "^6.2.6"
}, },
"dependencies": { "dependencies": {
"@auth/sveltekit": "^1.10.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"cron": "^4.3.2",
"postgres": "^3.4.7", "postgres": "^3.4.7",
"tailwindcss": "^4.1.11" "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,14 +1,31 @@
// db.js // 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) { export async function setAccountInTotal(accountId, total) {
return await db` return await db`
update account update account
set in_total = ${total} set in_total = ${total}
where id = ${accountId} where id = ${accountId}
` `;
} }
export async function setAccountHide(accountId, hide) { export async function setAccountHide(accountId, hide) {
@ -16,56 +33,37 @@ export async function setAccountHide(accountId, hide) {
update account update account
set hide = ${hide} set hide = ${hide}
where id = ${accountId} where id = ${accountId}
` `;
} }
export async function deleteBudget(id) { export async function addBudget(name, notes) {
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` const result = await db`
insert into budget (name, amount, notes) insert into budget (name, notes)
values (${name}, ${amount}, ${notes}) values (${name}, ${notes})
returning id returning id
` `;
// result = Result [{ id: 1 }] // result = Result [{ id: 1 }]
return result[0].id return result[0].id;
} }
export async function deleteBudget(id) { export async function deleteBudget(id) {
const result = await db` const result = await db`
delete from budget update budget
set delete = true
where id = ${id} where id = ${id}
` `;
// result = Result [{ id: 1 }] // result = Result [{ id: 1 }]
return result 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) { export async function updateBudget(id, name, amount, notes) {
@ -75,59 +73,90 @@ export async function updateBudget(id, name, amount, notes) {
amount = ${amount}, amount = ${amount},
notes = ${notes} notes = ${notes}
where id = ${id} where id = ${id}
` `;
// result = Result [{ id: 1 }] // result = Result [{ id: 1 }]
return result return result;
} }
export async function getBudgetTransactions(id) { export async function getBudgetTransactions(id) {
// Fetch all transactions associated with a specific budget // Fetch all transactions associated with a specific budget
try { try {
const transactions = await db` let transactions = await db`
select select
transaction.id as id, transaction.id as id,
transaction.posted as posted, transaction.posted as posted,
transaction.amount as amount, transaction.amount as amount,
transaction.description as description, transaction.description as description,
transaction.pending as pending, 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.amount as budget_amount,
budget_transaction.id as budget_transaction_id budget_transaction.id as budget_transaction_id
from budget_transaction from budget_transaction
join transaction on budget_transaction.transaction_id = transaction.id join transaction on budget_transaction.transaction_id = transaction.id
where budget_transaction.budget_id = ${id} where budget_transaction.budget_id = ${id}
order by transaction.posted desc order by transaction.posted desc
` `;
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" }, ...] // transactions = Result [{ id: 1, posted: 1633036800, amount: 50.00, description: "Grocery Store", pending: false, notes: "Weekly groceries" }, ...]
return { transactions } return { transactions };
} } catch {
catch { return [];
await createBudgetTransactionTable();
return []
} }
} }
export async function deleteBudgetTransaction(budgetId, transactionId) { 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 // Delete a transaction from a budget
const result = await db` const result = await db`
delete from budget_transaction delete from budget_transaction
where budget_id = ${budgetId} and transaction_id = ${transactionId} where id = ${id}
` `;
// result = Result [{ id: 1 }] // result = Result [{ id: 1 }]
return result return result;
}
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;
}
} }
export async function addBudgetTransaction(budgetId, transactionId, amount) {
// Add a transaction to a budget // Add a transaction to a budget
const result = await db` const result = await db`
insert into budget_transaction (budget_id, transaction_id, amount) insert into budget_transaction (budget_id, transaction_id, amount, notes, rule_id)
values (${budgetId}, ${transactionId}, ${amount}) values (${budgetId}, ${transactionId}, ${amount}, ${notes}, ${ruleId ?? null})
returning id returning id
` `;
// result = Result [{ id: 1 }] // result = Result [{ id: 1 }]
return result[0].id return result[0].id;
} }
export async function getBudgets() { export async function getBudgets() {
@ -135,17 +164,31 @@ export async function getBudgets() {
select select
budget.id as id, budget.id as id,
budget.name as name, budget.name as name,
budget.amount as amount, budget.sum as sum,
budget.notes as notes budget.notes as notes
from budget from budget_with_sum as budget
WHERE budget.delete is false WHERE budget.delete is false
` `;
if (!budgets) {
await createBudgetTable();
return await getBudgets()
}
// budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...]
return budgets 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() { export async function getDeletedBudgets() {
@ -153,17 +196,12 @@ export async function getDeletedBudgets() {
select select
budget.id as id, budget.id as id,
budget.name as name, budget.name as name,
budget.amount as amount,
budget.notes as notes budget.notes as notes
from budget from budget
WHERE budget.delete is true WHERE budget.delete is true
` `;
if (!budgets) {
await createBudgetTable();
return await getBudgets()
}
// budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] // budgets = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...]
return budgets return budgets;
} }
export async function getAccount(id) { export async function getAccount(id) {
@ -184,9 +222,9 @@ export async function getAccount(id) {
from account from account
left join org on account.org_id = org.id left join org on account.org_id = org.id
where account.id = ${id} where account.id = ${id}
` `;
if (!account || account.length === 0) { if (!account || account.length === 0) {
return null return null;
} }
return account[0]; return account[0];
} }
@ -199,9 +237,23 @@ export async function getAccounts(age) {
balance balance
from account from account
where hide is false where hide is false
` `;
// users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...]
return accounts 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) { export async function getHiddenAccounts(age) {
@ -211,9 +263,9 @@ export async function getHiddenAccounts(age) {
account.name as name account.name as name
from account from account
where account.hide is true where account.hide is true
` `;
// users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...] // users = Result [{ name: "Walter", age: 80 }, { name: 'Murray', age: 68 }, ...]
return accounts return accounts;
} }
export async function getTransactions(accountId) { export async function getTransactions(accountId) {
@ -224,15 +276,17 @@ export async function getTransactions(accountId) {
transaction.amount as amount, transaction.amount as amount,
transaction.description as description, transaction.description as description,
transaction.pending as pending, transaction.pending as pending,
transaction.notes as notes transaction.notes as notes,
transaction.payee as payee
from transaction from transaction
where account_id = ${accountId} where account_id = ${accountId}
order by posted desc order by posted desc
` `;
transactions = transactions.map((t) => ({ transactions = transactions.map((t) => ({
...t, date: new Date(t.posted * 1000) ...t,
date: new Date(t.posted * 1000)
})); }));
return transactions return transactions;
} }
export async function setTransactionNote(transactionId, note) { export async function setTransactionNote(transactionId, note) {
@ -240,8 +294,8 @@ export async function setTransactionNote(transactionId, note) {
update transaction update transaction
set notes = ${note} set notes = ${note}
where id = ${transactionId} where id = ${transactionId}
` `;
return result return result;
} }
export async function updateAccounts(data) { export async function updateAccounts(data) {
@ -308,7 +362,7 @@ export async function updateAccounts(data) {
} }
console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn); console.log(`Preparing to upsert transaction: ${txn.id} with data:`, txn);
await db` await db`
insert into transaction (id, account_id, posted, amount, description, pending, transacted_at) insert into transaction (id, account_id, posted, amount, description, pending, transacted_at, payee)
values ( values (
${txn.id}, ${txn.id},
${account.id}, ${account.id},
@ -316,7 +370,8 @@ export async function updateAccounts(data) {
${txn.amount ?? null}, ${txn.amount ?? null},
${txn.description ?? null}, ${txn.description ?? null},
${txn.pending ?? false}, ${txn.pending ?? false},
${txn.transacted_at ?? 0} ${txn.transacted_at ?? 0},
${txn.payee ?? null}
) )
on conflict (id) do update set on conflict (id) do update set
account_id = excluded.account_id, account_id = excluded.account_id,
@ -324,7 +379,8 @@ export async function updateAccounts(data) {
amount = excluded.amount, amount = excluded.amount,
description = excluded.description, description = excluded.description,
pending = excluded.pending, pending = excluded.pending,
transacted_at = excluded.transacted_at transacted_at = excluded.transacted_at,
payee = excluded.payee
`; `;
} }
} }
@ -336,5 +392,187 @@ export async function updateAccounts(data) {
} }
} }
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,10 +1,13 @@
const url =
'https://19443E0E8171E175EC5DA0C69B35DD50197F234B9A74C00D27FD606121257ECF:DAA3702E2100CFFD3B544251E6D755E86B1EDDFBFCC7F6FA9CE77AB3677E60DE@beta-bridge.simplefin.org/simplefin';
export async function fetchAccounts(url, startDate) { export async function fetchAccounts(startDate) {
const { username, password, origin, pathname } = new URL(url); const { username, password, origin, pathname } = new URL(url);
const start = Math.floor(startDate.getTime() / 1000); const apiUrl = `${origin}${pathname}/accounts?start-date=${startDate}`;
const apiUrl = `${origin}${pathname}/accounts?start-date=${start}`;
const headers = {}; const headers = {};
console.log(`Fetching accounts from: ${apiUrl}`);
if (username && password) { if (username && password) {
headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`); headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`);
} }

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 { getAccounts, getBudgets, getTotal } from '$lib/db';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export async function load({ params }) { export async function load({ params }) {
let accounts = await getAccounts(); let accounts = await getAccounts();
let budgets = await getBudgets(); let budgets = await getBudgets();
let total = await getTotal();
return { accounts, budgets }; return { accounts, budgets, total };
} }

View File

@ -2,72 +2,55 @@
import '../app.css'; import '../app.css';
let { children, data } = $props(); let { children, data } = $props();
let budgets = $derived(data.budgets); let budgets = $derived(data.budgets);
let newBudget = $state({ let total = $derived(data.total);
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');
}
}
</script> </script>
<div class="drawer lg:drawer-open"> <div class="drawer lg:drawer-open">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" /> <input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col m-5"> <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()} {@render children()}
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden"> Open drawer </label>
</div> </div>
<div class="drawer-side"> <div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label> <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"> <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> <li><div class="divider">Budgets</div></li>
{#each budgets as budget} {#each budgets as budget}
<li> <li>
<a href={`/budget/${budget.id}`}> <a href={`/budget/${budget.id}`}>
{budget.name} ({budget.amount}) {budget.name} ({budget.sum})
</a> </a>
</li> </li>
{/each} {/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> <li><div class="divider">Accounts</div></li>
{#each data.accounts as account} {#each data.accounts as account}
<li> <li>
@ -77,31 +60,8 @@
</li> </li>
{/each} {/each}
<li><div class="divider"></div></li> <li><div class="divider"></div></li>
<li><a href="/rules">Rules</a></li>
<li><a href="/settings">Settings</a></li> <li><a href="/settings">Settings</a></li>
</ul> </ul>
</div> </div>
</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 { error } from '@sveltejs/kit';
import { getAccount, getTransactions } from '$lib/db'; import { getAccount, getTransactions, getBudgets, getBudgetTransactionsForAccount } from '$lib/db';
/** @type {import('./$types').PageServerLoad} */ /** @type {import('./$types').PageServerLoad} */
export async function load({ params }) { 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) { if (transactions) {
return {transactions, account}; return { transactions, account, budgets, slug, budgetTransactions };
} }
error(404, 'Not found'); error(404, 'Not found');
} }

View File

@ -1,9 +1,12 @@
<script> <script>
import { EditSymbol } from '$lib/editSymbol.svelte';
import { settingsSymbol } from '$lib/settingsSymbol.svelte';
let { data } = $props(); let { data } = $props();
console.log(data);
let trans = $derived(data.transactions); let trans = $derived(data.transactions);
let budgets = $derived(data.budgets);
let budgetTransactions = $derived(data.budgetTransactions);
let notes = $state(''); let notes = $state('');
let currentTransaction = $state(null); let currentTransaction = $state({ budget_id: null, amount: 0, notes: '' });
let account = $derived(data.account); let account = $derived(data.account);
let hide = $derived(account?.hide || false); let hide = $derived(account?.hide || false);
let inTotal = $derived(account?.in_total || false); let inTotal = $derived(account?.in_total || false);
@ -53,39 +56,12 @@
</script> </script>
<div class="flex mb-4"> <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-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-64 grow">{account?.balance}</div>
<div class="w-14 flex-none text-right"> <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" <button class="btn btn-square btn-ghost">{@render settingsSymbol()} </button>
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>
</div> </div>
@ -94,18 +70,55 @@
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Payee</th>
<th>Description</th> <th>Description</th>
<th>Amount</th> <th>Amount</th>
<th>Notes</th> <th>Notes</th>
<th>Budgets</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each trans as transaction} {#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.date.toDateString()}</td>
<td>{transaction.payee ?? ''}</td>
<td>{transaction.description}</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>{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> </tr>
{/each} {/each}
</tbody> </tbody>
@ -116,14 +129,66 @@
<form method="dialog"> <form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form> </form>
<h3>{currentTransaction?.description}</h3> <fieldset class="fieldset">
<h4>${currentTransaction?.amount}</h4> <p class="label">{currentTransaction?.description}</p>
<p>{currentTransaction?.date?.toDateString()}</p> <p class="label">${currentTransaction?.amount}</p>
<div> <p class="label">{currentTransaction?.date?.toDateString()}</p>
<legend class="fieldset-legend">Notes</legend>
<textarea bind:value={notes} class="textarea w-100"></textarea> <textarea bind:value={notes} class="textarea w-100"></textarea>
<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> </div>
<button onclick={() => saveNotes()} class="btn btn-primary mt-4">Save</button> <form method="dialog" class="modal-backdrop">
</div> <button>close</button>
</form>
</dialog> </dialog>
<dialog id="settings_modal" class="modal"> <dialog id="settings_modal" class="modal">
@ -145,4 +210,7 @@
</fieldset> </fieldset>
<button onclick={() => saveSettings()} class="btn btn-primary mt-4">Save</button> <button onclick={() => saveSettings()} class="btn btn-primary mt-4">Save</button>
</div> </div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog> </dialog>

View File

@ -1,4 +1,4 @@
import { deleteBudget } from '$lib/db.js'; import { deleteBudget, restoreBudget } from '$lib/db.js';
export function DELETE({ params }) { export function DELETE({ params }) {
const { slug } = params; const { slug } = params;
@ -9,3 +9,11 @@ export function DELETE({ params }) {
.then(() => new Response(`Budget with slug ${slug} deleted successfully.`)) .then(() => new Response(`Budget with slug ${slug} deleted successfully.`))
.catch(err => new Response(`Error deleting budget: ${err.message}`, { status: 500 })); .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 { 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(startDate);
const res = await fetchAccounts(url, new Date("2025-07-03")) await updateAccounts(res);
return new Response(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 { error } from '@sveltejs/kit';
import { getBudgetTransactions } from '$lib/db.js'; import { getBudget, getBudgetTransactions } from '$lib/db.js';
export async function load({ params }) { export async function load({ params }) {
const { slug } = params; const { slug } = params;
console.log(`Loading transactions for budget: ${slug}`); console.log(`Loading transactions for budget: ${slug}`);
const transactions = await getBudgetTransactions(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> <script>
import { EditSymbol } from '$lib/editSymbol.svelte';
import { TrashBin } from '$lib/trashbin.svelte';
let { data } = $props(); let { data } = $props();
let budget = $derived(data.budgets.find((b) => b.id == data.slug) || {}); let budget = $derived(data.budget);
let transactions = $derived(data.transactions || []); let transactions = $derived(data.transactions.transactions || []);
let toDeleteBudget = $state(null); let newData = $state({
amount: 0,
notes: ''
});
let toDelete = $state(null);
let toDeleteName = $state('');
async function deleteBudget() { async function saveTransaction() {
if (toDeleteBudget === budget.name) { let res = await fetch(`/api/budget/${budget.id}/transaction`, {
let res = await fetch(`/api/budget/${budget.id}`, { method: 'PATCH',
method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} },
body: JSON.stringify({
amount: $state.snapshot(newData.amount),
notes: $state.snapshot(newData.notes)
})
}); });
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.');
}
} }
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> </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="flex mb-4">
<div class="w-32 flex-none justify-bottom"><h1 class="text-2xl font-bold">{budget.name}</h1></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 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> </div>
<div class="">
<div class="mb-4"> <h2>Notes:</h2>
<p class="text-sm">Notes: {budget.notes}</p> <p>{budget.notes}</p>
</div> </div>
<table class="min-w-full border border-gray-300"> <table class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">
<thead> <thead>
<tr class=""> <tr class="">
<th class="px-4 py-2 border-b">Date</th> <th>Date</th>
<th class="px-4 py-2 border-b">Description</th> <th>Description</th>
<th class="px-4 py-2 border-b">Amount</th> <th>Amount</th>
<th>Notes</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each transactions as txn} {#each transactions as txn}
<tr> <tr>
<td class="px-4 py-2 border-b">{txn.date}</td> <td>{txn.date.toDateString()}</td>
<td class="px-4 py-2 border-b">{txn.description}</td> <td>{txn.description}</td>
<td class="px-4 py-2 border-b">${txn.amount}</td> <td>${txn.budget_amount}</td>
</tr> <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} {/each}
{#if transactions.length === 0} {#if transactions.length === 0}
<tr> <tr>
<td class="px-4 py-2 border-b text-center" colspan="3">No transactions found.</td> <td colspan="3">No transactions found.</td>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
</table> </table>
<dialog id="EditBudgetTransactionModal" class="modal">
<dialog id="DeleteBudgetModal" class="modal">
<div class="modal-box"> <div class="modal-box">
<form method="dialog"> <fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <legend class="fieldset-legend">Edit</legend>
</form>
<p>Are you sure you want to delete <code>{budget.name}</code>? type its name in the box below if so.</p> <label class="label">Amount</label>
<div>
<input <input
bind:value={toDeleteBudget} bind:value={newData.amount}
type="text" type="text"
placeholder="Budget Name" placeholder="Amount"
class="input input-bordered w-full max-w-xs" class="input input-bordered w-full max-w-xs"
/> />
<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> </div>
<button onclick={()=>deleteBudget()} class="btn btn-primary mt-4">Delete</button> <form method="dialog" class="modal-backdrop">
</div> <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> </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 { getHiddenAccounts, getDeletedBudgets, getBudgets } from "$lib/db";
import { get } from "svelte/store";
export async function load({ params }) { export async function load({ params }) {
let accounts = await getHiddenAccounts(); 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,16 +1,75 @@
<script> <script>
import { deleteBudgetForm } from './deleteBudget.svelte';
import { loadingModal } from '$lib/loadingModal.svelte';
let { data } = $props(); let { data } = $props();
let accounts = $derived(data.accounts || []); let accounts = $derived(data.accounts || []);
let deletedBudgets = $derived(data.deletedBudgets || []);
let budgets = $derived(data.budgets || []); 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() { async function update() {
loading = true;
let res = await fetch('/api/simplefin/update', { let res = await fetch('/api/simplefin/update', {
method: 'POST' 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> </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"> <div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">Settings</h1> <h1 class="text-2xl font-bold">Settings</h1>
@ -28,10 +87,35 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h2 class="text-xl font-semibold">Deleted Budgets</h2> <h2 class="text-xl font-semibold">Budgets</h2>
<div>
{#if budgets.length > 0} {#if budgets.length > 0}
<ul> <ul>
{#each budgets as budget} {#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> <li><a href={`/budget/${budget.id}`}>{budget.name}</a></li>
{/each} {/each}
</ul> </ul>
@ -40,3 +124,30 @@
{/if} {/if}
</div> </div>
</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) { export async function POST(request) {
let body = await request.request.json(); let body = await request.request.json();
let res = await setTransactionNote(request.params.slug, body.notes); 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}), { return new Response(JSON.stringify({success: true}), {
headers: { headers: {
'Content-Type': 'application/json' '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;