diff --git a/frontend-web/bun.lock b/frontend-web/bun.lock index c5ecde0..e512821 100644 --- a/frontend-web/bun.lock +++ b/frontend-web/bun.lock @@ -6,7 +6,10 @@ "name": "frontend-web", "dependencies": { "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/oxanium": "^5.2.8", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", @@ -162,6 +165,8 @@ "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], + "@fontsource-variable/oxanium": ["@fontsource-variable/oxanium@5.2.8", "", {}, "sha512-W3HWxRLXVB6yox3dgm1DIGOp98pz8KglwiM6/2BsMopvZrg98mXI9citFWz2pUX0FOOLwf8fmHxX+KrIn+6BoA=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -180,16 +185,28 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], @@ -200,7 +217,9 @@ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], @@ -218,6 +237,8 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], @@ -378,6 +399,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -444,6 +467,8 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.266", "", {}, "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], @@ -498,6 +523,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -626,10 +653,16 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-router": ["react-router@7.10.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw=="], "react-router-dom": ["react-router-dom@7.10.1", "", { "dependencies": { "react-router": "7.10.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "recharts": ["recharts@3.5.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA=="], "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], @@ -670,6 +703,8 @@ "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -684,6 +719,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], @@ -706,20 +745,20 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -740,16 +779,6 @@ "recharts/immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], - "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/frontend-web/package.json b/frontend-web/package.json index c64b8df..944356d 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -11,7 +11,10 @@ }, "dependencies": { "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/oxanium": "^5.2.8", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", diff --git a/frontend-web/src/components/AddAccountDialog.tsx b/frontend-web/src/components/AddAccountDialog.tsx new file mode 100644 index 0000000..0d4b597 --- /dev/null +++ b/frontend-web/src/components/AddAccountDialog.tsx @@ -0,0 +1,239 @@ +import {useState} from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; +import {useAppDispatch, useAppSelector, addAccount} from '@/store'; + +interface AddAccountDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function AddAccountDialog({open, onOpenChange}: AddAccountDialogProps) { + const dispatch = useAppDispatch(); + const {categories} = useAppSelector(state => state.debts); + + const [formData, setFormData] = useState({ + name: '', + categoryId: '', + institution: '', + accountNumber: '', + originalBalance: '', + currentBalance: '', + interestRate: '', + minimumPayment: '', + dueDay: '', + notes: '', + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const now = new Date().toISOString(); + const account = { + id: crypto.randomUUID(), + name: formData.name, + categoryId: formData.categoryId, + institution: formData.institution, + accountNumber: formData.accountNumber || undefined, + originalBalance: parseFloat(formData.originalBalance) || 0, + currentBalance: parseFloat(formData.currentBalance) || parseFloat(formData.originalBalance) || 0, + interestRate: parseFloat(formData.interestRate) || 0, + minimumPayment: parseFloat(formData.minimumPayment) || 0, + dueDay: parseInt(formData.dueDay) || 1, + notes: formData.notes || undefined, + createdAt: now, + updatedAt: now, + }; + + dispatch(addAccount(account)); + onOpenChange(false); + setFormData({ + name: '', + categoryId: '', + institution: '', + accountNumber: '', + originalBalance: '', + currentBalance: '', + interestRate: '', + minimumPayment: '', + dueDay: '', + notes: '', + }); + }; + + const updateField = (field: string, value: string) => { + setFormData(prev => ({...prev, [field]: value})); + }; + + return ( + + + + Add Debt Account + + Add a new debt account to track your payoff progress + + +
+
+
+ + updateField('name', e.target.value)} + className="input-depth" + required + /> +
+ +
+ + +
+ +
+ + updateField('institution', e.target.value)} + className="input-depth" + required + /> +
+ +
+ + updateField('accountNumber', e.target.value.replace(/\D/g, ''))} + className="input-depth" + /> +
+ +
+
+ + updateField('originalBalance', e.target.value)} + className="input-depth" + required + /> +
+
+ + updateField('currentBalance', e.target.value)} + className="input-depth" + /> +
+
+ +
+
+ + updateField('interestRate', e.target.value)} + className="input-depth" + /> +
+
+ + updateField('minimumPayment', e.target.value)} + className="input-depth" + /> +
+
+ +
+ + updateField('dueDay', e.target.value)} + className="input-depth" + /> +
+
+ + + + + +
+
+
+ ); +} + diff --git a/frontend-web/src/components/Layout.tsx b/frontend-web/src/components/Layout.tsx index 0338c24..5911c8d 100644 --- a/frontend-web/src/components/Layout.tsx +++ b/frontend-web/src/components/Layout.tsx @@ -1,53 +1,50 @@ import {NavLink, Outlet} from 'react-router-dom'; -import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'; +import {TrendingUp, CreditCard, FileText, Users} from 'lucide-react'; const navItems = [ - {to: '/', label: 'Net Worth', icon: '◈'}, - {to: '/debts', label: 'Debts', icon: '◇'}, - {to: '/invoices', label: 'Invoices', icon: '▤'}, - {to: '/clients', label: 'Clients', icon: '◉'}, + {to: '/', label: 'Net Worth', icon: TrendingUp}, + {to: '/debts', label: 'Debts', icon: CreditCard}, + {to: '/invoices', label: 'Invoices', icon: FileText}, + {to: '/clients', label: 'Clients', icon: Users}, ]; export default function Layout() { return ( - -
- {/* Sidebar */} - +
+ {/* Sidebar */} + - {/* Main content */} -
- -
-
- + {/* Main content */} +
+ +
+
); } - diff --git a/frontend-web/src/components/ui/dialog.tsx b/frontend-web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..6cb123b --- /dev/null +++ b/frontend-web/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/frontend-web/src/components/ui/select.tsx b/frontend-web/src/components/ui/select.tsx new file mode 100644 index 0000000..25e5439 --- /dev/null +++ b/frontend-web/src/components/ui/select.tsx @@ -0,0 +1,187 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/frontend-web/src/fonts.d.ts b/frontend-web/src/fonts.d.ts index e1c167a..e2f522b 100644 --- a/frontend-web/src/fonts.d.ts +++ b/frontend-web/src/fonts.d.ts @@ -1,2 +1,3 @@ declare module '@fontsource-variable/geist'; +declare module '@fontsource-variable/oxanium'; diff --git a/frontend-web/src/index.css b/frontend-web/src/index.css index 3f2358e..28ccabd 100644 --- a/frontend-web/src/index.css +++ b/frontend-web/src/index.css @@ -114,10 +114,12 @@ * { @apply border-border outline-ring/50; } + html { + font-size: 17px; + } body { @apply bg-background text-foreground; - font-family: 'Geist Variable', system-ui, sans-serif; - font-feature-settings: 'ss01' 1, 'ss02' 1; + font-family: 'Oxanium Variable', system-ui, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/frontend-web/src/main.tsx b/frontend-web/src/main.tsx index 99c4fb9..fdaf4f1 100644 --- a/frontend-web/src/main.tsx +++ b/frontend-web/src/main.tsx @@ -2,7 +2,7 @@ import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import {Provider} from 'react-redux'; import {store} from './store'; -import '@fontsource-variable/geist'; +import '@fontsource-variable/oxanium'; import './index.css'; import App from './App.tsx'; diff --git a/frontend-web/src/pages/ClientsPage.tsx b/frontend-web/src/pages/ClientsPage.tsx index d907541..b6aa39a 100644 --- a/frontend-web/src/pages/ClientsPage.tsx +++ b/frontend-web/src/pages/ClientsPage.tsx @@ -1,10 +1,4 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; import {useAppSelector} from '@/store'; @@ -14,47 +8,38 @@ export default function ClientsPage() { const getClientStats = (clientId: string) => { const clientInvoices = invoices.filter(i => i.clientId === clientId); const totalBilled = clientInvoices.reduce((sum, i) => sum + i.total, 0); - const outstanding = clientInvoices - .filter(i => i.status === 'sent' || i.status === 'overdue') - .reduce((sum, i) => sum + i.total, 0); - return {totalBilled, outstanding, invoiceCount: clientInvoices.length}; + const outstanding = clientInvoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((sum, i) => sum + i.total, 0); + return {totalBilled, outstanding, count: clientInvoices.length}; }; - const formatCurrency = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value); + const fmt = (value: number) => + new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); return ( -
-
-
-

Clients

-

Manage your customers and clients

-
- +
+ {/* Header */} +
+

Clients

+
- {/* Summary */} -
+ {/* Summary Cards */} +
- - Total Clients + +

Total Clients

- -

{clients.length}

+ +

{clients.length}

- - Active This Month + +

With Active Invoices

- -

- { - clients.filter(c => { - const stats = getClientStats(c.id); - return stats.invoiceCount > 0; - }).length - } + +

+ {clients.filter(c => getClientStats(c.id).count > 0).length}

@@ -62,46 +47,39 @@ export default function ClientsPage() { {/* Clients List */} - - All Clients - View and manage client information + + All Clients - + {clients.length === 0 ? (
-

No clients added yet

-

- Add your first client to start creating invoices -

+

No clients yet

+
) : ( -
+
{clients.map(client => { const stats = getClientStats(client.id); return ( -
+
-

{client.name}

-

- {client.email} - {client.company && ` · ${client.company}`} +

{client.name}

+

+ {client.email}{client.company && ` · ${client.company}`}

-
-
-

Total Billed

-

{formatCurrency(stats.totalBilled)}

+
+
+

Billed

+

{fmt(stats.totalBilled)}

-
-

Outstanding

-

{formatCurrency(stats.outstanding)}

+
+

Outstanding

+

{fmt(stats.outstanding)}

-
-

Invoices

-

{stats.invoiceCount}

+
+

Invoices

+

{stats.count}

@@ -114,4 +92,3 @@ export default function ClientsPage() {
); } - diff --git a/frontend-web/src/pages/DebtsPage.tsx b/frontend-web/src/pages/DebtsPage.tsx index 80ae4b9..3b1b287 100644 --- a/frontend-web/src/pages/DebtsPage.tsx +++ b/frontend-web/src/pages/DebtsPage.tsx @@ -1,113 +1,253 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import {useState} from 'react'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; -import {useAppSelector} from '@/store'; +import {useAppSelector, type DebtAccount} from '@/store'; +import AddAccountDialog from '@/components/AddAccountDialog'; + +type ViewMode = 'all' | 'by-category' | 'by-account'; export default function DebtsPage() { - const {debts} = useAppSelector(state => state.debts); + const [viewMode, setViewMode] = useState('by-category'); + const [addDialogOpen, setAddDialogOpen] = useState(false); + const {categories, accounts} = useAppSelector(state => state.debts); - const totalDebt = debts.reduce((sum, d) => sum + d.currentBalance, 0); - const totalMinPayment = debts.reduce((sum, d) => sum + d.minimumPayment, 0); + const totalDebt = accounts.reduce((sum, a) => sum + a.currentBalance, 0); + const totalMinPayment = accounts.reduce((sum, a) => sum + a.minimumPayment, 0); + const totalOriginal = accounts.reduce((sum, a) => sum + a.originalBalance, 0); + const totalPaidDown = totalOriginal - totalDebt; - const formatCurrency = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value); + const fmt = (value: number) => + new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); + + const getCategoryById = (id: string) => categories.find(c => c.id === id); + const getAccountsByCategory = (categoryId: string) => accounts.filter(a => a.categoryId === categoryId); + const getCategoryTotal = (categoryId: string) => getAccountsByCategory(categoryId).reduce((sum, a) => sum + a.currentBalance, 0); + const getProgress = (account: DebtAccount) => + account.originalBalance > 0 ? ((account.originalBalance - account.currentBalance) / account.originalBalance) * 100 : 0; + + const categoriesWithDebt = categories.filter(c => getCategoryTotal(c.id) > 0); return ( -
-
-
-

Debt Management

-

Track and pay down your debts

+
+ {/* Header */} +
+

Debts

+
+ +
-
{/* Summary Cards */} -
+
- - Total Debt + +

Total Debt

- -

{formatCurrency(totalDebt)}

+ +

{fmt(totalDebt)}

- - Monthly Minimum + +

Paid Down

- -

{formatCurrency(totalMinPayment)}

+ +

{fmt(totalPaidDown)}

- - Active Debts + +

Monthly Min

- -

{debts.length}

+ +

{fmt(totalMinPayment)}

+
+
+ + +

Accounts

+
+ +

{accounts.length}

- {/* Debts List */} - - - Your Debts - Manage and track payments - - - {debts.length === 0 ? ( -
-

No debts added yet

-

- Add your first debt to start tracking your payoff progress -

-
- ) : ( -
- {debts.map(debt => { - const progress = - ((debt.originalBalance - debt.currentBalance) / debt.originalBalance) * 100; - return ( -
-
-
-

{debt.name}

-

{debt.lender}

-
-
-

{formatCurrency(debt.currentBalance)}

-

- {debt.interestRate}% APR -

-
+ {/* View Toggle */} +
+ {(['by-category', 'by-account', 'all'] as const).map(mode => ( + + ))} +
+ + {/* Content */} + {accounts.length === 0 ? ( + + +

No debt accounts yet

+ +
+
+ ) : viewMode === 'by-category' ? ( +
+ {categoriesWithDebt.map(category => { + const categoryAccounts = getAccountsByCategory(category.id); + const categoryTotal = getCategoryTotal(category.id); + const categoryOriginal = categoryAccounts.reduce((sum, a) => sum + a.originalBalance, 0); + const categoryProgress = categoryOriginal > 0 ? ((categoryOriginal - categoryTotal) / categoryOriginal) * 100 : 0; + + return ( + + +
+
+ {category.name} + + {categoryAccounts.length} account{categoryAccounts.length !== 1 ? 's' : ''} +
-
-
+
+
+
+
+
+ {categoryProgress.toFixed(0)}% +
+ {fmt(categoryTotal)}
-

- {progress.toFixed(1)}% paid off -

- ); - })} + + +
+ {categoryAccounts.map(account => ( + + ))} +
+
+ + ); + })} +
+ ) : viewMode === 'by-account' ? ( +
+ {accounts.map(account => { + const category = getCategoryById(account.categoryId); + const progress = getProgress(account); + return ( + + +
+
+

{account.name}

+

{account.institution}

+
+ {category?.name} +
+

{fmt(account.currentBalance)}

+
+
+
+
+ {progress.toFixed(0)}% +
+
+ {account.interestRate}% APR + {fmt(account.minimumPayment)}/mo +
+ + + ); + })} +
+ ) : ( + + +
+ {accounts.map(account => ( + + ))}
- )} -
-
+
+
+ )} + + {/* Category Summary */} + {accounts.length > 0 && ( + + + Debt by Category + + +
+ {categories + .map(c => ({c, total: getCategoryTotal(c.id), count: getAccountsByCategory(c.id).length})) + .filter(x => x.count > 0) + .sort((a, b) => b.total - a.total) + .map(({c, total}) => ( +
+
+ {c.name} + {fmt(total)} + ({((total / totalDebt) * 100).toFixed(0)}%) +
+ ))} +
+ + + )} + +
); } +function AccountRow({ + account, + fmt, + getProgress, + showCategory = false, + getCategoryById, +}: { + account: DebtAccount; + fmt: (value: number) => string; + getProgress: (account: DebtAccount) => number; + showCategory?: boolean; + getCategoryById?: (id: string) => {name: string} | undefined; +}) { + const progress = getProgress(account); + const category = getCategoryById?.(account.categoryId); + + return ( +
+
+
+ {account.name} + {showCategory && category && ( + {category.name} + )} +
+

+ {account.institution}{account.accountNumber && ` ••${account.accountNumber}`} · {account.interestRate}% APR +

+
+
+
+
+
+
+ {progress.toFixed(0)}% +
+ {fmt(account.currentBalance)} +
+
+ ); +} diff --git a/frontend-web/src/pages/InvoicesPage.tsx b/frontend-web/src/pages/InvoicesPage.tsx index 58ed59b..467f069 100644 --- a/frontend-web/src/pages/InvoicesPage.tsx +++ b/frontend-web/src/pages/InvoicesPage.tsx @@ -1,10 +1,4 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; import {useAppSelector} from '@/store'; import {format} from 'date-fns'; @@ -12,21 +6,13 @@ import {format} from 'date-fns'; export default function InvoicesPage() { const {invoices, clients} = useAppSelector(state => state.invoices); - const getClientName = (clientId: string) => { - const client = clients.find(c => c.id === clientId); - return client?.name ?? 'Unknown Client'; - }; + const getClientName = (clientId: string) => clients.find(c => c.id === clientId)?.name ?? 'Unknown'; - const totalOutstanding = invoices - .filter(i => i.status === 'sent' || i.status === 'overdue') - .reduce((sum, i) => sum + i.total, 0); + const totalOutstanding = invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((sum, i) => sum + i.total, 0); + const totalPaid = invoices.filter(i => i.status === 'paid').reduce((sum, i) => sum + i.total, 0); - const totalPaid = invoices - .filter(i => i.status === 'paid') - .reduce((sum, i) => sum + i.total, 0); - - const formatCurrency = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value); + const fmt = (value: number) => + new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); const statusStyles: Record = { draft: 'bg-muted text-muted-foreground', @@ -37,84 +23,68 @@ export default function InvoicesPage() { }; return ( -
-
-
-

Invoices

-

Create and manage invoices

-
- +
+ {/* Header */} +
+

Invoices

+
{/* Summary Cards */} -
+
- - Outstanding + +

Outstanding

- -

{formatCurrency(totalOutstanding)}

+ +

{fmt(totalOutstanding)}

- - Paid (All Time) + +

Paid (All Time)

- -

{formatCurrency(totalPaid)}

+ +

{fmt(totalPaid)}

- - Total Invoices + +

Total Invoices

- -

{invoices.length}

+ +

{invoices.length}

{/* Invoices List */} - - Recent Invoices - View and manage your invoices + + Recent Invoices - + {invoices.length === 0 ? (
-

No invoices created yet

-

- Create your first invoice to get started -

+

No invoices yet

+
) : ( -
+
{invoices.map(invoice => ( -
-
-
-

{invoice.invoiceNumber}

-

- {getClientName(invoice.clientId)} -

+
+
+
+ {invoice.invoiceNumber} + + {invoice.status} +
+

{getClientName(invoice.clientId)}

-
-
-

{formatCurrency(invoice.total)}

-

- Due {format(new Date(invoice.dueDate), 'MMM d, yyyy')} -

-
- - {invoice.status} - +
+

{fmt(invoice.total)}

+

Due {format(new Date(invoice.dueDate), 'MMM d')}

))} @@ -125,4 +95,3 @@ export default function InvoicesPage() {
); } - diff --git a/frontend-web/src/pages/NetWorthPage.tsx b/frontend-web/src/pages/NetWorthPage.tsx index 6604ad4..7d275fa 100644 --- a/frontend-web/src/pages/NetWorthPage.tsx +++ b/frontend-web/src/pages/NetWorthPage.tsx @@ -1,86 +1,69 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Button} from '@/components/ui/button'; import {useAppSelector} from '@/store'; -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; - -// Demo data for the chart -const demoData = [ - {month: 'Jan', netWorth: 45000}, - {month: 'Feb', netWorth: 47500}, - {month: 'Mar', netWorth: 46200}, - {month: 'Apr', netWorth: 52000}, - {month: 'May', netWorth: 55800}, - {month: 'Jun', netWorth: 58200}, -]; +import {AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer} from 'recharts'; +import {format} from 'date-fns'; export default function NetWorthPage() { - const {assets, liabilities} = useAppSelector(state => state.netWorth); + const {assets, liabilities, snapshots} = useAppSelector(state => state.netWorth); + + const chartData = snapshots.map(s => ({ + month: format(new Date(s.date), 'MMM'), + netWorth: s.netWorth, + })); const totalAssets = assets.reduce((sum, a) => sum + a.value, 0); const totalLiabilities = liabilities.reduce((sum, l) => sum + l.balance, 0); const netWorth = totalAssets - totalLiabilities; - const formatCurrency = (value: number) => - new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(value); + const fmt = (value: number) => + new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0}).format(value); return ( -
-
-

Net Worth

-

Track your wealth over time

+
+ {/* Header */} +
+

Net Worth

+
{/* Summary Cards */} -
+
- - Total Assets + +

Total Assets

- -

{formatCurrency(totalAssets)}

+ +

{fmt(totalAssets)}

- - Total Liabilities + +

Total Liabilities

- -

{formatCurrency(totalLiabilities)}

+ +

{fmt(totalLiabilities)}

- - Net Worth + +

Net Worth

- -

{formatCurrency(netWorth)}

+ +

{fmt(netWorth)}

{/* Chart */} - - - Net Worth Over Time + + + Net Worth Over Time - -
+ +
- + @@ -88,32 +71,14 @@ export default function NetWorthPage() { - - `$${value / 1000}k`} - /> + + `$${v / 1000}k`} /> [formatCurrency(value), 'Net Worth']} - /> - [fmt(value), 'Net Worth']} /> +
@@ -121,55 +86,51 @@ export default function NetWorthPage() { {/* Assets & Liabilities */} -
+
- -
- Assets - What you own -
- + + Assets + - + {assets.length === 0 ? ( -

No assets added yet

+

No assets added yet

) : ( -
    +
    {assets.map(asset => ( -
  • - {asset.name} - {formatCurrency(asset.value)} -
  • +
    +
    + {asset.name} + {asset.type} +
    + {fmt(asset.value)} +
    ))} -
+
)} - -
- Liabilities - What you owe -
- + + Liabilities + - + {liabilities.length === 0 ? ( -

No liabilities added yet

+

No liabilities added yet

) : ( -
    +
    {liabilities.map(liability => ( -
  • - {liability.name} - {formatCurrency(liability.balance)} -
  • +
    +
    + {liability.name} + {liability.type.replace('_', ' ')} +
    + {fmt(liability.balance)} +
    ))} -
+
)}
@@ -177,4 +138,3 @@ export default function NetWorthPage() {
); } - diff --git a/frontend-web/src/store/index.ts b/frontend-web/src/store/index.ts index ea1d4fe..164d74b 100644 --- a/frontend-web/src/store/index.ts +++ b/frontend-web/src/store/index.ts @@ -28,15 +28,19 @@ export type {Asset, Liability, NetWorthSnapshot, NetWorthState} from './slices/n export { setLoading as setDebtsLoading, setError as setDebtsError, - addDebt, - updateDebt, - removeDebt, + addCategory, + updateCategory, + removeCategory, + setCategories, + addAccount, + updateAccount, + removeAccount, + setAccounts, addPayment, removePayment, - setDebts, setPayments, } from './slices/debtsSlice'; -export type {Debt, DebtPayment, DebtsState} from './slices/debtsSlice'; +export type {DebtCategory, DebtAccount, DebtPayment, DebtsState} from './slices/debtsSlice'; // Invoices slice export { diff --git a/frontend-web/src/store/slices/debtsSlice.ts b/frontend-web/src/store/slices/debtsSlice.ts index 478b976..e81d3b2 100644 --- a/frontend-web/src/store/slices/debtsSlice.ts +++ b/frontend-web/src/store/slices/debtsSlice.ts @@ -1,37 +1,153 @@ import {createSlice, type PayloadAction} from '@reduxjs/toolkit'; -export interface Debt { +export interface DebtCategory { id: string; name: string; - type: 'credit_card' | 'personal_loan' | 'auto_loan' | 'student_loan' | 'mortgage' | 'other'; + color: string; + createdAt: string; +} + +export interface DebtAccount { + id: string; + name: string; + categoryId: string; + institution: string; + accountNumber?: string; // last 4 digits originalBalance: number; currentBalance: number; interestRate: number; minimumPayment: number; dueDay: number; - lender: string; + notes?: string; createdAt: string; updatedAt: string; } export interface DebtPayment { id: string; - debtId: string; + accountId: string; amount: number; date: string; note?: string; } export interface DebtsState { - debts: Debt[]; + categories: DebtCategory[]; + accounts: DebtAccount[]; payments: DebtPayment[]; isLoading: boolean; error: string | null; } +// Default categories +const defaultCategories: DebtCategory[] = [ + {id: 'credit-cards', name: 'Credit Cards', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'personal-loans', name: 'Personal Loans', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'auto-loans', name: 'Auto Loans', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'student-loans', name: 'Student Loans', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'mortgage', name: 'Mortgage', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'medical', name: 'Medical', color: '#6b7280', createdAt: new Date().toISOString()}, + {id: 'other', name: 'Other', color: '#6b7280', createdAt: new Date().toISOString()}, +]; + +// Mock data for development +const mockAccounts: DebtAccount[] = [ + { + id: 'cc1', + name: 'Chase Sapphire Preferred', + categoryId: 'credit-cards', + institution: 'Chase', + accountNumber: '4521', + originalBalance: 8500, + currentBalance: 3200, + interestRate: 21.99, + minimumPayment: 95, + dueDay: 15, + createdAt: '2024-01-15', + updatedAt: '2024-12-01', + }, + { + id: 'cc2', + name: 'Amex Blue Cash', + categoryId: 'credit-cards', + institution: 'American Express', + accountNumber: '1008', + originalBalance: 4200, + currentBalance: 1850, + interestRate: 19.24, + minimumPayment: 55, + dueDay: 22, + createdAt: '2024-02-10', + updatedAt: '2024-12-01', + }, + { + id: 'cc3', + name: 'Citi Double Cash', + categoryId: 'credit-cards', + institution: 'Citibank', + accountNumber: '7732', + originalBalance: 2800, + currentBalance: 950, + interestRate: 18.49, + minimumPayment: 35, + dueDay: 8, + createdAt: '2024-03-05', + updatedAt: '2024-12-01', + }, + { + id: 'al1', + name: 'Tesla Model 3 Loan', + categoryId: 'auto-loans', + institution: 'Tesla Finance', + accountNumber: '9901', + originalBalance: 42000, + currentBalance: 15000, + interestRate: 4.99, + minimumPayment: 650, + dueDay: 1, + createdAt: '2021-06-15', + updatedAt: '2024-12-01', + }, + { + id: 'sl1', + name: 'Federal Student Loan', + categoryId: 'student-loans', + institution: 'Dept of Education', + originalBalance: 45000, + currentBalance: 28000, + interestRate: 5.5, + minimumPayment: 320, + dueDay: 25, + createdAt: '2018-09-01', + updatedAt: '2024-12-01', + }, + { + id: 'pl1', + name: 'Home Improvement Loan', + categoryId: 'personal-loans', + institution: 'SoFi', + accountNumber: '3344', + originalBalance: 15000, + currentBalance: 8500, + interestRate: 8.99, + minimumPayment: 285, + dueDay: 12, + createdAt: '2023-08-20', + updatedAt: '2024-12-01', + }, +]; + +const mockPayments: DebtPayment[] = [ + {id: 'p1', accountId: 'cc1', amount: 500, date: '2024-11-15', note: 'Extra payment'}, + {id: 'p2', accountId: 'cc2', amount: 200, date: '2024-11-22'}, + {id: 'p3', accountId: 'al1', amount: 650, date: '2024-12-01'}, + {id: 'p4', accountId: 'sl1', amount: 320, date: '2024-11-25'}, +]; + const initialState: DebtsState = { - debts: [], - payments: [], + categories: defaultCategories, + accounts: mockAccounts, + payments: mockPayments, isLoading: false, error: null, }; @@ -46,26 +162,61 @@ const debtsSlice = createSlice({ setError: (state, action: PayloadAction) => { state.error = action.payload; }, - addDebt: (state, action: PayloadAction) => { - state.debts.push(action.payload); + // Category actions + addCategory: (state, action: PayloadAction) => { + state.categories.push(action.payload); }, - updateDebt: (state, action: PayloadAction) => { - const index = state.debts.findIndex(d => d.id === action.payload.id); - if (index !== -1) state.debts[index] = action.payload; + updateCategory: (state, action: PayloadAction) => { + const index = state.categories.findIndex(c => c.id === action.payload.id); + if (index !== -1) state.categories[index] = action.payload; }, - removeDebt: (state, action: PayloadAction) => { - state.debts = state.debts.filter(d => d.id !== action.payload); - state.payments = state.payments.filter(p => p.debtId !== action.payload); + removeCategory: (state, action: PayloadAction) => { + state.categories = state.categories.filter(c => c.id !== action.payload); + // Move accounts in this category to 'other' + state.accounts.forEach(a => { + if (a.categoryId === action.payload) a.categoryId = 'other'; + }); }, + setCategories: (state, action: PayloadAction) => { + state.categories = action.payload; + }, + // Account actions + addAccount: (state, action: PayloadAction) => { + state.accounts.push(action.payload); + }, + updateAccount: (state, action: PayloadAction) => { + const index = state.accounts.findIndex(a => a.id === action.payload.id); + if (index !== -1) state.accounts[index] = action.payload; + }, + removeAccount: (state, action: PayloadAction) => { + state.accounts = state.accounts.filter(a => a.id !== action.payload); + state.payments = state.payments.filter(p => p.accountId !== action.payload); + }, + setAccounts: (state, action: PayloadAction) => { + state.accounts = action.payload; + }, + // Payment actions addPayment: (state, action: PayloadAction) => { state.payments.push(action.payload); + // Update account balance + const account = state.accounts.find(a => a.id === action.payload.accountId); + if (account) { + account.currentBalance = Math.max(0, account.currentBalance - action.payload.amount); + account.updatedAt = new Date().toISOString(); + } }, removePayment: (state, action: PayloadAction) => { + const payment = state.payments.find(p => p.id === action.payload); + if (payment) { + // Restore account balance + const account = state.accounts.find(a => a.id === payment.accountId); + if (account) { + account.currentBalance += payment.amount; + account.updatedAt = new Date().toISOString(); + } + } state.payments = state.payments.filter(p => p.id !== action.payload); }, - setDebts: (state, action: PayloadAction) => { - state.debts = action.payload; - }, setPayments: (state, action: PayloadAction) => { state.payments = action.payload; }, @@ -75,14 +226,17 @@ const debtsSlice = createSlice({ export const { setLoading, setError, - addDebt, - updateDebt, - removeDebt, + addCategory, + updateCategory, + removeCategory, + setCategories, + addAccount, + updateAccount, + removeAccount, + setAccounts, addPayment, removePayment, - setDebts, setPayments, } = debtsSlice.actions; export default debtsSlice.reducer; - diff --git a/frontend-web/src/store/slices/invoicesSlice.ts b/frontend-web/src/store/slices/invoicesSlice.ts index 755192d..036d8c1 100644 --- a/frontend-web/src/store/slices/invoicesSlice.ts +++ b/frontend-web/src/store/slices/invoicesSlice.ts @@ -42,9 +42,133 @@ export interface InvoicesState { error: string | null; } +// Mock data for development +const mockClients: Client[] = [ + { + id: 'c1', + name: 'Acme Corp', + email: 'billing@acme.com', + phone: '555-0100', + company: 'Acme Corporation', + address: '123 Business Ave, Suite 400, San Francisco, CA 94102', + createdAt: '2024-01-10', + }, + { + id: 'c2', + name: 'TechStart Inc', + email: 'accounts@techstart.io', + phone: '555-0200', + company: 'TechStart Inc', + address: '456 Innovation Blvd, Austin, TX 78701', + createdAt: '2024-02-15', + }, + { + id: 'c3', + name: 'Sarah Mitchell', + email: 'sarah@mitchell.design', + company: 'Mitchell Design Studio', + createdAt: '2024-03-22', + }, + { + id: 'c4', + name: 'Global Media LLC', + email: 'finance@globalmedia.com', + phone: '555-0400', + company: 'Global Media LLC', + address: '789 Media Row, New York, NY 10001', + createdAt: '2024-04-08', + }, +]; + +const mockInvoices: Invoice[] = [ + { + id: 'inv1', + invoiceNumber: 'INV-2024-001', + clientId: 'c1', + status: 'paid', + issueDate: '2024-10-01', + dueDate: '2024-10-31', + lineItems: [ + {id: 'li1', description: 'Web Development - October', quantity: 80, unitPrice: 150, total: 12000}, + {id: 'li2', description: 'Hosting & Maintenance', quantity: 1, unitPrice: 500, total: 500}, + ], + subtotal: 12500, + tax: 0, + total: 12500, + createdAt: '2024-10-01', + updatedAt: '2024-10-15', + }, + { + id: 'inv2', + invoiceNumber: 'INV-2024-002', + clientId: 'c2', + status: 'paid', + issueDate: '2024-10-15', + dueDate: '2024-11-14', + lineItems: [ + {id: 'li3', description: 'Mobile App Development', quantity: 120, unitPrice: 175, total: 21000}, + ], + subtotal: 21000, + tax: 0, + total: 21000, + createdAt: '2024-10-15', + updatedAt: '2024-11-10', + }, + { + id: 'inv3', + invoiceNumber: 'INV-2024-003', + clientId: 'c1', + status: 'sent', + issueDate: '2024-11-01', + dueDate: '2024-12-01', + lineItems: [ + {id: 'li4', description: 'Web Development - November', quantity: 60, unitPrice: 150, total: 9000}, + {id: 'li5', description: 'API Integration', quantity: 20, unitPrice: 175, total: 3500}, + ], + subtotal: 12500, + tax: 0, + total: 12500, + createdAt: '2024-11-01', + updatedAt: '2024-11-01', + }, + { + id: 'inv4', + invoiceNumber: 'INV-2024-004', + clientId: 'c3', + status: 'overdue', + issueDate: '2024-10-20', + dueDate: '2024-11-20', + lineItems: [ + {id: 'li6', description: 'Brand Identity Design', quantity: 1, unitPrice: 4500, total: 4500}, + ], + subtotal: 4500, + tax: 0, + total: 4500, + createdAt: '2024-10-20', + updatedAt: '2024-10-20', + }, + { + id: 'inv5', + invoiceNumber: 'INV-2024-005', + clientId: 'c4', + status: 'draft', + issueDate: '2024-12-01', + dueDate: '2024-12-31', + lineItems: [ + {id: 'li7', description: 'Video Production', quantity: 5, unitPrice: 2000, total: 10000}, + {id: 'li8', description: 'Motion Graphics', quantity: 10, unitPrice: 500, total: 5000}, + ], + subtotal: 15000, + tax: 0, + total: 15000, + createdAt: '2024-12-01', + updatedAt: '2024-12-01', + }, +]; + const initialState: InvoicesState = { - clients: [], - invoices: [], + clients: mockClients, + invoices: mockInvoices, isLoading: false, error: null, }; diff --git a/frontend-web/src/store/slices/netWorthSlice.ts b/frontend-web/src/store/slices/netWorthSlice.ts index 4445647..fd39c7e 100644 --- a/frontend-web/src/store/slices/netWorthSlice.ts +++ b/frontend-web/src/store/slices/netWorthSlice.ts @@ -32,10 +32,35 @@ export interface NetWorthState { error: string | null; } +// Mock data for development +const mockAssets: Asset[] = [ + {id: 'a1', name: 'Chase Checking', type: 'cash', value: 12500, updatedAt: '2024-12-01'}, + {id: 'a2', name: 'Ally Savings', type: 'cash', value: 35000, updatedAt: '2024-12-01'}, + {id: 'a3', name: 'Fidelity 401k', type: 'investment', value: 145000, updatedAt: '2024-12-01'}, + {id: 'a4', name: 'Vanguard Brokerage', type: 'investment', value: 52000, updatedAt: '2024-12-01'}, + {id: 'a5', name: 'Primary Residence', type: 'property', value: 425000, updatedAt: '2024-12-01'}, + {id: 'a6', name: '2021 Tesla Model 3', type: 'vehicle', value: 28000, updatedAt: '2024-12-01'}, +]; + +const mockLiabilities: Liability[] = [ + {id: 'l1', name: 'Mortgage', type: 'mortgage', balance: 320000, updatedAt: '2024-12-01'}, + {id: 'l2', name: 'Auto Loan', type: 'loan', balance: 15000, updatedAt: '2024-12-01'}, + {id: 'l3', name: 'Student Loans', type: 'loan', balance: 28000, updatedAt: '2024-12-01'}, +]; + +const mockSnapshots: NetWorthSnapshot[] = [ + {id: 's1', date: '2024-07-01', totalAssets: 650000, totalLiabilities: 380000, netWorth: 270000}, + {id: 's2', date: '2024-08-01', totalAssets: 665000, totalLiabilities: 375000, netWorth: 290000}, + {id: 's3', date: '2024-09-01', totalAssets: 680000, totalLiabilities: 370000, netWorth: 310000}, + {id: 's4', date: '2024-10-01', totalAssets: 685000, totalLiabilities: 368000, netWorth: 317000}, + {id: 's5', date: '2024-11-01', totalAssets: 692000, totalLiabilities: 365000, netWorth: 327000}, + {id: 's6', date: '2024-12-01', totalAssets: 697500, totalLiabilities: 363000, netWorth: 334500}, +]; + const initialState: NetWorthState = { - assets: [], - liabilities: [], - snapshots: [], + assets: mockAssets, + liabilities: mockLiabilities, + snapshots: mockSnapshots, isLoading: false, error: null, };