diff --git a/Backend/eng.traineddata b/Backend/eng.traineddata new file mode 100644 index 0000000..6d11002 Binary files /dev/null and b/Backend/eng.traineddata differ diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 48a3bdc..b20f591 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", @@ -26,9 +27,11 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongoose": "^8.8.2", + "multer": "^2.0.0", "path": "^0.12.7", "swagger": "^0.0.1", "swagger-ui-express": "^5.0.1", + "tesseract.js": "^6.0.1", "yaml": "^2.6.1", "yamljs": "^0.3.0" }, @@ -42,6 +45,7 @@ "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.12", "@types/node": "^22.9.1", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.7", @@ -79,15 +83,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -267,9 +271,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -277,9 +281,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -297,27 +301,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -566,15 +570,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -625,14 +629,14 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -657,6 +661,15 @@ "node": ">=12" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1141,14 +1154,35 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -1438,6 +1472,16 @@ "mongoose": "*" } }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", @@ -1687,6 +1731,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -1766,9 +1816,10 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -1939,6 +1990,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2043,9 +2100,9 @@ } }, "node_modules/bson": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", - "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -2061,9 +2118,19 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2332,6 +2399,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -2407,6 +2489,12 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2988,26 +3076,22 @@ } }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/formidable/node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "engines": { - "node": ">=8" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3382,6 +3466,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -3546,6 +3636,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4640,6 +4742,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -4676,6 +4787,18 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -4686,13 +4809,13 @@ } }, "node_modules/mongodb": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", - "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -4700,7 +4823,7 @@ }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", @@ -4732,23 +4855,24 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.8.4", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.4.tgz", - "integrity": "sha512-yJbn695qCsqDO+xyPII29x2R7flzXhxCDv09mMZPSGllf0sm4jKw3E9s9uvQ9hjO6bL2xjU8KKowYqcY9eSTMQ==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.0.tgz", + "integrity": "sha512-WFKsY1q12ScGabnZWUB9c/QzZmz/ESorrV27OembB7Gz6rrh9m3GA4Srsv1uvW1s9AHO5DeZ6DdUTyF9zyNERQ==", + "license": "MIT", "dependencies": { - "bson": "^6.7.0", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.10.0", + "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -4818,6 +4942,24 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5051,6 +5193,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5263,6 +5414,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -5374,6 +5531,27 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5387,6 +5565,12 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5712,6 +5896,29 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5948,6 +6155,30 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/tesseract.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", + "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^6.0.0", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", + "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==", + "license": "Apache-2.0" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6003,15 +6234,15 @@ } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/ts-jest": { @@ -6142,6 +6373,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", @@ -6287,6 +6524,12 @@ "makeerror": "1.0.12" } }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -6297,16 +6540,16 @@ } }, "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/which": { @@ -6371,6 +6614,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -6464,6 +6716,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } } } } diff --git a/Backend/package.json b/Backend/package.json index c5c7e32..4dc7169 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -5,12 +5,13 @@ "scripts": { "start": "tsc index.ts", "start:dev": "npx tsc & nodemon", - "test":"jest" + "test": "jest" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", @@ -28,9 +29,11 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "mongoose": "^8.8.2", + "multer": "^2.0.0", "path": "^0.12.7", "swagger": "^0.0.1", "swagger-ui-express": "^5.0.1", + "tesseract.js": "^6.0.1", "yaml": "^2.6.1", "yamljs": "^0.3.0" }, @@ -44,6 +47,7 @@ "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.7", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.12", "@types/node": "^22.9.1", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.7", diff --git a/Backend/src/BusinessLogic_Layer/config/dotenvConfig.ts b/Backend/src/BusinessLogic_Layer/config/dotenvConfig.ts index 9b29a4b..4bf2e12 100644 --- a/Backend/src/BusinessLogic_Layer/config/dotenvConfig.ts +++ b/Backend/src/BusinessLogic_Layer/config/dotenvConfig.ts @@ -8,6 +8,8 @@ export const JWT_SECRET = process.env.JWT_SECRET as string; export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ; export const COOKIE_NAME = process.env.COOKIE_NAME ; export const NODE_ENV = process.env.NODE_ENV ; +export const geminiApiKey = process.env.GEMINI_API_KEY || ""; +export const PORT = process.env.PORT || 5000; // Validate all required environment variables @@ -30,3 +32,40 @@ if (!process.env.COOKIE_NAME) { if (!process.env.NODE_ENV) { console.warn("NODE_ENV is not set. Using default value 'development'."); } + + +// import * as dotenv from "dotenv"; + +// // Load environment variables from .env file +// dotenv.config(); + +// // Structured configuration object +// export const config = { +// port: process.env.PORT || 5000, +// geminiApiKey: process.env.GEMINI_API_KEY || "", +// jwtSecret: process.env.JWT_SECRET as string, +// jwtExpiration: process.env.JWT_EXPIRATION || "1h", +// cookieName: process.env.COOKIE_NAME || "authToken", +// nodeEnv: process.env.NODE_ENV || "development", +// }; + +// // Validate required environment variables +// if (!config.jwtSecret) { +// throw new Error("JWT_SECRET is not set in .env"); +// } + +// if (!process.env.JWT_EXPIRATION) { +// console.warn("JWT_EXPIRATION is not set. Using default value '1h'."); +// } + +// if (!process.env.COOKIE_NAME) { +// console.warn("COOKIE_NAME is not set. Using default value 'authToken'."); +// } + +// if (!process.env.NODE_ENV) { +// console.warn("NODE_ENV is not set. Using default value 'development'."); +// } + +// if (!config.geminiApiKey) { +// console.warn("GEMINI_API_KEY is not set. Gemini services will not function correctly."); +// } diff --git a/Backend/src/BusinessLogic_Layer/controllers/chatbot.controllers.ts b/Backend/src/BusinessLogic_Layer/controllers/chatbot.controllers.ts new file mode 100644 index 0000000..40d38b0 --- /dev/null +++ b/Backend/src/BusinessLogic_Layer/controllers/chatbot.controllers.ts @@ -0,0 +1,26 @@ +import { Request, Response } from "express"; +import { JwtPayload } from "jsonwebtoken"; +import GeminiService from "../services/chatbot.services"; + +class ChatController { + constructor(private geminiService: GeminiService) {} + + async chatWithAI(req: Request & { user?: JwtPayload }, res: Response): Promise { + try { + const { message } = req.body; + + if (!message) { + res.status(400).json({ error: "Message is required" }); + return; + } + + const aiResponse = await this.geminiService.generateAIResponse(message); + res.status(200).json({ response: aiResponse }); + } catch (error: any) { + console.error("ChatWithAI Error:", error); + res.status(500).json({ error: "Internal Server Error" }); + } + } +} + +export default ChatController; diff --git a/Backend/src/BusinessLogic_Layer/controllers/reg.controllers.ts b/Backend/src/BusinessLogic_Layer/controllers/reg.controllers.ts index b2eab74..94c768f 100644 --- a/Backend/src/BusinessLogic_Layer/controllers/reg.controllers.ts +++ b/Backend/src/BusinessLogic_Layer/controllers/reg.controllers.ts @@ -4,10 +4,10 @@ import regService from '../services/Reg.services'; class regController { static async register (req:Request,res:Response) { - const {name ,email,phone ,password}=req.body; + const {name ,email,phone ,password, securityQuestion, securityAnswer}=req.body; try { console.log("here "); - const user =await regService.registerUser(name ,email,phone,password); + const user =await regService.registerUser(name ,email,phone,password, securityQuestion, securityAnswer); res.status(201).json({message:'User registered successfully!',user}); } catch (error:any) { diff --git a/Backend/src/BusinessLogic_Layer/controllers/security.controller.ts b/Backend/src/BusinessLogic_Layer/controllers/security.controller.ts new file mode 100644 index 0000000..2d33e27 --- /dev/null +++ b/Backend/src/BusinessLogic_Layer/controllers/security.controller.ts @@ -0,0 +1,69 @@ +import { Request, Response } from 'express'; +import User from '../../Database_Layer/models/user.schema'; +import jwt from 'jsonwebtoken'; +import { JWT_SECRET } from '../config/dotenvConfig'; + +class SecurityController { + // Get the security question for the logged-in user + static async getSecurityQuestion(req: Request, res: Response) { + try { + const token = req.cookies[process.env.COOKIE_NAME as string]; + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const decoded = jwt.verify(token, JWT_SECRET as string) as { id: string }; + const user = await User.findById(decoded.id); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + res.json({ question: user.securityQuestion }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } + } + + // Verify the security answer + static async verifySecurityAnswer(req: Request, res: Response) { + try { + const { answer } = req.body; + const token = req.cookies[process.env.COOKIE_NAME as string]; + + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const decoded = jwt.verify(token, JWT_SECRET as string) as { id: string }; + const user = await User.findById(decoded.id); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const isAnswerCorrect = await user.compareSecurityAnswer(answer); + if (!isAnswerCorrect) { + return res.status(400).json({ error: 'Incorrect security answer' }); + } + + // Generate a new token for full authentication + const newToken = jwt.sign({ id: user._id, verified: true }, JWT_SECRET as string, { + expiresIn: '1h', + }); + + // Update the cookie with the new token + res.cookie(process.env.COOKIE_NAME as string, newToken, { + httpOnly: true, + sameSite: 'strict', + maxAge: 3600000, + }); + + res.json({ verified: true }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } + } +} + +export default SecurityController; \ No newline at end of file diff --git a/Backend/src/BusinessLogic_Layer/controllers/transaction.controller.ts b/Backend/src/BusinessLogic_Layer/controllers/transaction.controller.ts index a8af44e..9f55d16 100644 --- a/Backend/src/BusinessLogic_Layer/controllers/transaction.controller.ts +++ b/Backend/src/BusinessLogic_Layer/controllers/transaction.controller.ts @@ -64,6 +64,8 @@ class TransactionController { const transaction = await this.transactionService.addTransaction({ ...req.body, userId: user.id }); res.status(201).json(transaction); } catch (err: unknown) { + console.error("Error adding transaction:", (err as Error).message); // ← add this + res.status(500).json({ error: (err as Error).message }); } } @@ -86,6 +88,26 @@ class TransactionController { } } + + + async scanReceipt(req: Request & { user?: JwtPayload }, res: Response) { + try { + const user = req.user; + if (!user || !user.id) { + res.status(401).json({ error: "User not authenticated or invalid token" }); + return; + } + + if (!req.file) { + res.status(400).json({ error: "No file uploaded" }); + return; + } + + const result = await this.transactionService.processReceipt(req.file.path, user.id); + res.status(200).json(result); + } catch (err: unknown) { + res.status(500).json({ error: (err as Error).message }); + }} } export default TransactionController; diff --git a/Backend/src/BusinessLogic_Layer/controllers/vis.controller.ts b/Backend/src/BusinessLogic_Layer/controllers/vis.controller.ts index e0aadf3..a80715b 100644 --- a/Backend/src/BusinessLogic_Layer/controllers/vis.controller.ts +++ b/Backend/src/BusinessLogic_Layer/controllers/vis.controller.ts @@ -142,6 +142,23 @@ class VisController { } } //================================================================================================= +//updates + +async getIncomeBySource(req: Request & { user?: JwtPayload }, res: Response): Promise { + try { + const userId = req.user?.id; + if (!userId) { + res.status(401).json({ error: "Sorry Unauthorized :(" }); + return; + } + + //Pass query params to support month/year filter + const incomeSources = await this.visService.getIncomeBySource(userId, req.query); + res.status(200).json({ incomeSources }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } +} } export default VisController; diff --git a/Backend/src/BusinessLogic_Layer/routes/chatbot.routes.ts b/Backend/src/BusinessLogic_Layer/routes/chatbot.routes.ts new file mode 100644 index 0000000..8829505 --- /dev/null +++ b/Backend/src/BusinessLogic_Layer/routes/chatbot.routes.ts @@ -0,0 +1,19 @@ +import express from "express"; +import ChatController from "../controllers/chatbot.controllers"; +import GeminiService from "../services/chatbot.services"; + +const router = express.Router(); + +// Instantiate service and controller +const geminiService = new GeminiService(); +const chatController = new ChatController(geminiService); + +// POST: Handle chat interaction +router.post("/", (req, res) => chatController.chatWithAI(req, res)); + +// Optional GET route to help testers +router.get("/", (req, res) => { + res.send("This is a POST API. Use Thunder Client or Postman to test it."); +}); + +export default router; diff --git a/Backend/src/BusinessLogic_Layer/routes/security.routes.ts b/Backend/src/BusinessLogic_Layer/routes/security.routes.ts new file mode 100644 index 0000000..5db29fa --- /dev/null +++ b/Backend/src/BusinessLogic_Layer/routes/security.routes.ts @@ -0,0 +1,17 @@ +import express from 'express'; +import SecurityController from '../controllers/security.controller'; +import authenticateToken from '../Middleware/AuthMiddleware'; + +const router = express.Router(); + +// Get security question +router.get('/security-question', authenticateToken, (req, res) => { + SecurityController.getSecurityQuestion(req, res); +}); + +// Verify security answer +router.post('/verify-security-question', authenticateToken, (req, res) => { + SecurityController.verifySecurityAnswer(req, res); +}); + +export default router; \ No newline at end of file diff --git a/Backend/src/BusinessLogic_Layer/routes/transaction.routes.ts b/Backend/src/BusinessLogic_Layer/routes/transaction.routes.ts index 9af7b17..c26eb24 100644 --- a/Backend/src/BusinessLogic_Layer/routes/transaction.routes.ts +++ b/Backend/src/BusinessLogic_Layer/routes/transaction.routes.ts @@ -3,7 +3,9 @@ import express from 'express' import TransactionController from "../controllers/transaction.controller"; import TransactionService from "../services/transaction.service"; import authenticateToken from '../Middleware/AuthMiddleware'; - +import multer from 'multer'; +import * as path from 'path'; +const upload = multer({ dest: path.join(__dirname, "../../uploads/") }); const router = express.Router(); const transactionService = new TransactionService(); @@ -21,5 +23,6 @@ router.post("/", authenticateToken, (req,res)=> transactionController.addTransac // Route to delete a transaction by ID for the authenticated user router.delete("/:id", authenticateToken, (req,res)=> transactionController.deleteTransaction(req,res)); - +// Existing transaction routes... +router.post("/scan", authenticateToken, upload.single("receipt"), (req, res) => transactionController.scanReceipt(req, res)); export default router; diff --git a/Backend/src/BusinessLogic_Layer/routes/vis.routes.ts b/Backend/src/BusinessLogic_Layer/routes/vis.routes.ts index e81fbc9..d4b002b 100644 --- a/Backend/src/BusinessLogic_Layer/routes/vis.routes.ts +++ b/Backend/src/BusinessLogic_Layer/routes/vis.routes.ts @@ -27,6 +27,8 @@ router.get('/spent-in-last-30-days', authenticateToken, (req, res) => visControl //================================================================================================= router.get('/spent-in-last-12-months', authenticateToken, (req, res) => visController.getSpentLast12Months(req, res)); //================================================================================================= +//updates +router.get('/income-by-source', authenticateToken, (req, res) => visController.getIncomeBySource(req, res)); export default router; diff --git a/Backend/src/BusinessLogic_Layer/services/Reg.services.ts b/Backend/src/BusinessLogic_Layer/services/Reg.services.ts index 5543c4a..5a9ebf9 100644 --- a/Backend/src/BusinessLogic_Layer/services/Reg.services.ts +++ b/Backend/src/BusinessLogic_Layer/services/Reg.services.ts @@ -3,18 +3,28 @@ import jwt from 'jsonwebtoken'; class regServies { constructor(){}; - static async registerUser(name:string ,email:string ,phone:string,password:string ) - { + static async registerUser( + name: string, + email: string, + phone: string, + password: string, + securityQuestion: string, + securityAnswer: string + ) { const user = await User.findOne({email}); - if (user) - { - throw new Error("Email already in use ") + if (user) { + throw new Error("Email already in use"); } - const newUser = new User ({name,email,phone,password}); + const newUser = new User({ + name, + email, + phone, + password, + securityQuestion, + securityAnswerHash: securityAnswer // Will be hashed by the pre-save hook + }); return await newUser.save(); - } - - } + export default regServies; diff --git a/Backend/src/BusinessLogic_Layer/services/chatbot.services.ts b/Backend/src/BusinessLogic_Layer/services/chatbot.services.ts new file mode 100644 index 0000000..10f6e6a --- /dev/null +++ b/Backend/src/BusinessLogic_Layer/services/chatbot.services.ts @@ -0,0 +1,75 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { geminiApiKey } from "../config/dotenvConfig"; + +class GeminiService { + private model; + + constructor() { + const genAI = new GoogleGenerativeAI(geminiApiKey); + this.model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); + } + + async generateAIResponse(userInput: string): Promise { + try { + const systemPrompt = `You are PennyWise, a friendly, privacy-respecting AI coach who helps everyday people organize their personal finances, build sensible budgets, and develop healthy money habits. + + Core purpose + • Give clear, step-by-step guidance on budgeting, saving, debt payoff, spending tracking, and goal-based planning. + • Turn raw transaction or income data (if the user provides it) into digestible insights—e.g., category breakdowns, cash-flow forecasts, or simple charts. + • Motivate users with positive, judgment-free language and actionable next steps. + + Tone & style + • Conversational, concise, and encouraging—think supportive coach, not stern accountant. + • Use plain language; avoid jargon unless you immediately define it. + • When discussing numbers, format currency in the user’s locale (e.g., “$1,250.00” or “€1 250,00”). + + Data handling & privacy + • Never store or display users’ sensitive data beyond the current session. + • If the user pastes account details, immediately redact account numbers (show last 4 digits only). + + Safety & compliance + • You are not a licensed financial advisor. Always include a short disclaimer when giving recommendations: + “This is general information, not personalized financial advice. Consider consulting a certified professional for decisions that could materially affect you.” + • Do not provide tax, legal, or investment advice that could be construed as professional guidance. + • Avoid predicting markets or guaranteeing outcomes. + + Best-practice behaviors + + Clarify before calculating. If a request is ambiguous (e.g., “Help me budget”), ask 1–2 narrowing questions (income frequency, goals, major fixed costs) before producing numbers. + + Break it down. Present budgets in tiered levels (needs, wants, savings/debt) or by customizable categories the user can tweak. + + Keep it interactive. End most answers with an inviting question (“Would you like to set a monthly savings target next?”) to encourage engagement. + + Reference dates explicitly. When talking about “this month,” state the exact month and year so users aren’t confused later. + + Provide sources sparingly. Link only reputable, user-friendly resources (e.g., government consumer sites) when further reading is helpful. + + Respect user emotion. If a user expresses stress or shame about money, respond empathetically first, then pivot to constructive steps. + + Decline unsafe requests. Politely refuse (with a brief explanation) if asked to perform illegal actions, share personal info, or give certain investment/tax instructions. + + Formatting cheatsheet + • Use bulleted or numbered lists for budgets and action plans. + • Bold high-level categories; regular text for sub-items. + • Highlight key figures with inline code formatting ($500) for quick scanning. + + Session-start greeting (example) + “Hi! I’m PennyWise, your budgeting sidekick. Tell me a bit about your income and goals, and we’ll craft a plan together.”`; + + + + const finalPrompt = `${systemPrompt}\nUser: ${userInput}\nAssistant`; + + const result = await this.model.generateContent(finalPrompt); + const response = await result.response; + + return response.text(); + } catch (err: any) { + console.error("Gemini API Error:", err); + throw new Error("Failed to generate AI response: " + err.message); + } + } +} + +export default GeminiService; diff --git a/Backend/src/BusinessLogic_Layer/services/transaction.service.ts b/Backend/src/BusinessLogic_Layer/services/transaction.service.ts index 9271aad..4142458 100644 --- a/Backend/src/BusinessLogic_Layer/services/transaction.service.ts +++ b/Backend/src/BusinessLogic_Layer/services/transaction.service.ts @@ -5,6 +5,8 @@ import Budget from '../../Database_Layer/models/budget.schema'; import User from '../../Database_Layer/models/user.schema'; import Category from "../../Database_Layer/models/category.schema"; import moment from 'moment'; +import * as Tesseract from 'tesseract.js'; +import * as fs from 'fs'; // solid/clean code paradigms used => @@ -27,24 +29,36 @@ private isValidObjectId(id: string): boolean { } async getAllTransactions(userId: string, queryParams: any) { - let filter: Record = { userId }; // Base filter for the user + let filter: Record = { userId }; // Base filter - // Apply date filter if provided - if (queryParams.date) { + // Filter by month and year (if both provided) + if (queryParams.month && queryParams.year) { + const year = parseInt(queryParams.year); + const month = parseInt(queryParams.month) - 1; + + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 1); + + filter.date = { $gte: startDate, $lt: endDate }; + } + + // Filter by relative date range (optional) + else if (queryParams.date) { filter.date = { $gte: this.getStartDate(queryParams.date) }; } - // Apply category filter if provided + // Filter by category name (optional) if (queryParams.category) { const categoryFilter = await this.getCategoryFilter(queryParams.category); if (categoryFilter) { - filter.category = { $in: categoryFilter }; // Match ObjectId(s) of categories + filter.category = { $in: categoryFilter }; } } return await this.transactionModel.find(filter).populate('category'); } + // Single responsibility: Extract logic to calculate start date private getStartDate(dateRange: string): Date { const currentDate = moment(); @@ -163,6 +177,85 @@ private isValidObjectId(id: string): boolean { } + + async processReceipt(filePath: string, userId: string) { + const { data: { text } } = await Tesseract.recognize(filePath, 'eng'); + fs.unlinkSync(filePath); // delete temp file + + const extracted = this.extractData(text); + if (!extracted.total || isNaN(extracted.total)) { + throw new Error("Failed to extract a valid total amount."); + } + + return { + amount: extracted.total, + date: extracted.date || new Date() + }; + } + + private extractData(text: string) { + const lines = text.split("\n").map(l => l.trim()).filter(Boolean); + const fullText = lines.join(" "); + + const priceRegex = /(\$|RM)?\s?(\d{1,3}(?:[.,]\d{2}))/gi; + const dateRegexNumeric = /\b\d{2}[\/\-\.]\d{2}[\/\-\.]\d{2,4}\b/; + const dateRegexNamed = /\b\d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{4}\b/i; + const dateTimeRegex = /\b\d{2}[\/\-\.]\d{2}[\/\-\.]\d{2,4} \d{1,2}:\d{2}(?::\d{2})?( ?(AM|PM))?\b/i; + + const totalLine = lines.find(l => + /(total\s+(rm)?|total\s+amt|nett total|net\s+total|total\s+payable|cash\s+tendered|amount\s+payable)/i.test(l) && + priceRegex.test(l) + ); + + let total: number | undefined; + if (totalLine) { + const priceMatch = [...totalLine.matchAll(priceRegex)]; + if (priceMatch.length > 0) { + const raw = priceMatch[priceMatch.length - 1][2].replace(",", "."); + total = parseFloat(raw); + } + } + + if (!total) { + const allPrices = [...fullText.matchAll(priceRegex)].map(m => parseFloat(m[2].replace(",", "."))); + total = Math.max(...allPrices.filter(n => !isNaN(n))); + } + + let rawDate: string | undefined; + const timeMatch = fullText.match(dateTimeRegex); + const namedMatch = fullText.match(dateRegexNamed); + const numericMatch = fullText.match(dateRegexNumeric); + + if (timeMatch) rawDate = timeMatch[0]; + else if (namedMatch) rawDate = namedMatch[0]; + else if (numericMatch) rawDate = numericMatch[0]; + + let date: Date | undefined; + + if (rawDate) { + const parts = rawDate.split(/[\/\-\. ]/); + if (parts.length >= 3 && parts[2].length === 4) { + const day = parseInt(parts[0]); + const month = isNaN(+parts[1]) ? this.monthToNumber(parts[1]) : parseInt(parts[1]) - 1; + const year = parseInt(parts[2]); + const hour = parts.length > 3 ? parseInt(parts[3]) : 0; + const minute = parts.length > 4 ? parseInt(parts[4]) : 0; + date = new Date(year, month, day, hour, minute); + } else { + date = new Date(rawDate); + } + } + + return { total, date }; + } + + private monthToNumber(month: string): number { + const months = { + jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, + jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 + }; + return months[month.toLowerCase().slice(0, 3) as keyof typeof months] ?? 0; + } } export default TransactionService; diff --git a/Backend/src/BusinessLogic_Layer/services/vis.service.ts b/Backend/src/BusinessLogic_Layer/services/vis.service.ts index cbaa11c..be43108 100644 --- a/Backend/src/BusinessLogic_Layer/services/vis.service.ts +++ b/Backend/src/BusinessLogic_Layer/services/vis.service.ts @@ -122,4 +122,54 @@ export class VisService { return transactions.reduce((sum, t) => sum + t.amount, 0); } //================================================================================================= +//updates + +async getIncomeBySource(userId: string, queryParams: any): Promise<{ category: string; total: number }[]> { + if (!userId) throw new Error("User ID is required"); + + let matchStage: any = { + userId: new mongoose.Types.ObjectId(userId), + type: "income", + }; + + // Add date filter based on month and year + if (queryParams.month && queryParams.year) { + const year = parseInt(queryParams.year); + const month = parseInt(queryParams.month) - 1; // JS months are 0-based + + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 1); + + matchStage.date = { $gte: startDate, $lt: endDate }; + } + + const result = await Transaction.aggregate([ + { $match: matchStage }, + { + $lookup: { + from: "categories", + localField: "category", + foreignField: "_id", + as: "categoryInfo", + } + }, + { $unwind: "$categoryInfo" }, + { + $group: { + _id: "$categoryInfo.category", + total: { $sum: "$amount" }, + }, + }, + { + $project: { + category: "$_id", + total: 1, + _id: 0, + }, + }, + ]); + + return result; + } + } diff --git a/Backend/src/Database_Layer/configdb.ts b/Backend/src/Database_Layer/configdb.ts index e3b9aca..e361edb 100644 --- a/Backend/src/Database_Layer/configdb.ts +++ b/Backend/src/Database_Layer/configdb.ts @@ -5,7 +5,7 @@ import mongoose from 'mongoose'; //dotenv.config(); // Get the database URL from environment variables -const dbURI: string = 'mongodb+srv://MRB:MARBN12@cluster0.83945.mongodb.net/financeTracker'; +const dbURI: string = 'mongodb+srv://MRB:Radwa234@cluster0.83945.mongodb.net/financeTracker'; // Database connection function const connectDB = async (): Promise => { diff --git a/Backend/src/Database_Layer/models/user.schema.ts b/Backend/src/Database_Layer/models/user.schema.ts index efaf65d..96419f1 100644 --- a/Backend/src/Database_Layer/models/user.schema.ts +++ b/Backend/src/Database_Layer/models/user.schema.ts @@ -8,9 +8,13 @@ export interface IUser extends Document { password: string; // User's hashed password phone: string; // User's phone number total_income: number; // User's total income + securityQuestion: string; // User's security question + securityAnswerHash: string; // Hashed security answer // Method to compare plain password with hashed password in the database comparePassword(password: string): Promise; + // Method to compare security answer with hashed answer in the database + compareSecurityAnswer(answer: string): Promise; } // Define the schema for the User model @@ -40,14 +44,25 @@ const userSchema = new mongoose.Schema({ type: Number, // Total income should be a number required: false, // It's optional }, + securityQuestion: { + type: String, + required: true + }, + securityAnswerHash: { + type: String, + required: true + } }); -// Pre-save hook to hash the password before saving the user document +// Pre-save hook to hash the password and security answer before saving the user document userSchema.pre('save', async function (next) { if (this.isModified('password')) { // Check if the password has been modified // Hash the password with bcrypt before saving this.password = await bcrypt.hash(this.password, 10); } + if (this.isModified('securityAnswerHash')) { + this.securityAnswerHash = await bcrypt.hash(this.securityAnswerHash, 10); + } next(); // Proceed to save the user document }); @@ -57,6 +72,11 @@ userSchema.methods.comparePassword = async function (password: string) { return await bcrypt.compare(password, this.password); }; +// Method to compare the entered security answer with the hashed answer in the database +userSchema.methods.compareSecurityAnswer = async function (answer: string) { + return await bcrypt.compare(answer, this.securityAnswerHash); +}; + // Create the User model with the IUser interface and schema const User = mongoose.model('User', userSchema); diff --git a/Backend/src/index.ts b/Backend/src/index.ts index 46bdf3d..405a215 100644 --- a/Backend/src/index.ts +++ b/Backend/src/index.ts @@ -8,9 +8,10 @@ import TransactionRouter from './BusinessLogic_Layer/routes/transaction.routes'; import cookieParser from 'cookie-parser'; import visRouter from './BusinessLogic_Layer/routes/vis.routes'; import CategoryRouter from './BusinessLogic_Layer/routes/category.routes'; +import chatbot from './BusinessLogic_Layer/routes/chatbot.routes'; +import securityRouter from './BusinessLogic_Layer/routes/security.routes'; import cors from 'cors'; - let express = require('express'); let connectDB = require('./Database_Layer/configdb'); import swaggerUi from 'swagger-ui-express'; @@ -50,12 +51,13 @@ app.use('/api/auth/login', loginRouter); app.use('/api/auth/logout', logoutRouter); app.use('/api/auth/profile', profileRouter); app.use('/api/auth/reg', regRouter); +app.use('/api/auth', securityRouter); app.use('/recommendation', RecommendationRouter); app.use('/budgets', BudgetRouter); app.use('/transactions', TransactionRouter); app.use('/analytics', visRouter); app.use('/category', CategoryRouter); - +app.use('/gemini', chatbot); // Start the server app.listen(PORT, () => { @@ -63,3 +65,9 @@ app.listen(PORT, () => { }).on('error', (err: any) => { console.error('Failed to start the server:', err); }); + +//cd Backend , npm i +// npm install @google/generative-ai +// npm install multer +// npm install --save-dev @types/multer +// npm install tesseract.js \ No newline at end of file diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index 711589e..9882f40 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -4046,6 +4046,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/playwright": { "version": "1.49.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", @@ -4093,16 +4103,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", diff --git a/Frontend/public/images/entrtainment.jpeg b/Frontend/public/images/entrtainment.jpeg new file mode 100644 index 0000000..ca4a377 Binary files /dev/null and b/Frontend/public/images/entrtainment.jpeg differ diff --git a/Frontend/public/images/gas_transportation.jpeg b/Frontend/public/images/gas_transportation.jpeg new file mode 100644 index 0000000..30aee71 Binary files /dev/null and b/Frontend/public/images/gas_transportation.jpeg differ diff --git a/Frontend/public/images/grocery.webp b/Frontend/public/images/grocery.webp new file mode 100644 index 0000000..0dc1441 Binary files /dev/null and b/Frontend/public/images/grocery.webp differ diff --git a/Frontend/public/images/grocery_net.jpeg b/Frontend/public/images/grocery_net.jpeg new file mode 100644 index 0000000..7201c81 Binary files /dev/null and b/Frontend/public/images/grocery_net.jpeg differ diff --git a/Frontend/public/images/health.jpeg b/Frontend/public/images/health.jpeg new file mode 100644 index 0000000..a49a7ee Binary files /dev/null and b/Frontend/public/images/health.jpeg differ diff --git a/Frontend/public/images/home.jpeg b/Frontend/public/images/home.jpeg new file mode 100644 index 0000000..473c503 Binary files /dev/null and b/Frontend/public/images/home.jpeg differ diff --git a/Frontend/public/images/misc_net.png b/Frontend/public/images/misc_net.png new file mode 100644 index 0000000..81a8feb Binary files /dev/null and b/Frontend/public/images/misc_net.png differ diff --git a/Frontend/public/images/misc_pos.png b/Frontend/public/images/misc_pos.png new file mode 100644 index 0000000..224064d Binary files /dev/null and b/Frontend/public/images/misc_pos.png differ diff --git a/Frontend/public/images/personalcare.jpeg b/Frontend/public/images/personalcare.jpeg new file mode 100644 index 0000000..1a8f427 Binary files /dev/null and b/Frontend/public/images/personalcare.jpeg differ diff --git a/Frontend/public/images/pets.png b/Frontend/public/images/pets.png new file mode 100644 index 0000000..ff5f1e5 Binary files /dev/null and b/Frontend/public/images/pets.png differ diff --git a/Frontend/public/images/shopping.jpeg b/Frontend/public/images/shopping.jpeg new file mode 100644 index 0000000..e8644c0 Binary files /dev/null and b/Frontend/public/images/shopping.jpeg differ diff --git a/Frontend/public/images/shopping_net.jpeg b/Frontend/public/images/shopping_net.jpeg new file mode 100644 index 0000000..7d09212 Binary files /dev/null and b/Frontend/public/images/shopping_net.jpeg differ diff --git a/Frontend/public/images/travel.jpg b/Frontend/public/images/travel.jpg new file mode 100644 index 0000000..40e562b Binary files /dev/null and b/Frontend/public/images/travel.jpg differ diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 70d9f78..7dfeec8 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -4,6 +4,7 @@ import TransactionPage from './pages/TransactionsPage'; import BudgetPage from './pages/BudgetPage'; import DashboardPage from './pages/DashboardPage'; import LoginPage from './pages/LoginPage'; +import SecurityQuestion from './components/SecurityQuestion'; function App() { @@ -13,6 +14,7 @@ function App() { } /> } /> + } /> } /> } /> } /> diff --git a/Frontend/src/components/AddTransaction.tsx b/Frontend/src/components/AddTransaction.tsx index 5bdd87f..da4aba1 100644 --- a/Frontend/src/components/AddTransaction.tsx +++ b/Frontend/src/components/AddTransaction.tsx @@ -1,10 +1,13 @@ import React, { useState } from 'react'; import { transactionsService } from '../services/transactions.service'; -import { Transaction } from '../types/transaction'; import { Category } from '../types/category'; -// import './AddTransaction.css'; // Import the CSS file +import { CreateTransactionDTO } from '../types/transaction'; -const AddTransaction: React.FC = () => { +interface AddTransactionProps { + onTransactionAdded?: () => void; // ✅ callback to trigger refresh +} + +const AddTransaction: React.FC = ({ onTransactionAdded }) => { const categories: Category[] = [ { _id: '674af54da65f08511321ea6d', category: 'Household Expenses', category_img: '' }, { _id: '674c457fef61f784396a65f4', category: 'entertainment', category_img: '' }, @@ -27,17 +30,18 @@ const AddTransaction: React.FC = () => { { _id: '6752fd44ef61f784396b0a9c', category: 'salary', category_img: '' }, ]; - const [formData, setFormData] = useState>>({ - type: 'income', - category: undefined, + const [formData, setFormData] = useState({ + type: 'income' as 'income' | 'expense', + category: undefined as Category | undefined, amount: 0, date: '', description: '', }); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [scanMessage, setScanMessage] = useState(''); const handleInputChange = ( e: React.ChangeEvent @@ -46,11 +50,32 @@ const AddTransaction: React.FC = () => { if (name === 'category') { const selectedCategory = categories.find((cat) => cat._id === value); setFormData({ ...formData, category: selectedCategory }); + } else if (name === 'amount') { + setFormData({ ...formData, amount: Number(value) }); } else { setFormData({ ...formData, [name]: value }); } }; + const handleReceiptUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + try { + setScanMessage('⏳ Scanning receipt...'); + const { amount, date } = await transactionsService.scanReceipt(file); + setFormData((prev) => ({ + ...prev, + amount, + date: date.slice(0, 10), + })); + setScanMessage(`✅ Scanned Amount: ${amount}, Date: ${date.slice(0, 10)}`); + } catch (err) { + console.error(err); + setScanMessage('❌ Failed to scan receipt.'); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -58,12 +83,27 @@ const AddTransaction: React.FC = () => { setSuccess(''); try { - const transactionData = { - ...formData, - category: formData.category?._id, - } as unknown as Omit; + if (!formData.category?._id) { + setError('Please select a category'); + setLoading(false); + return; + } + + const transactionData: CreateTransactionDTO = { + type: formData.type, + category: formData.category._id, + amount: formData.amount, + date: formData.date, + description: formData.description || '', + }; await transactionsService.addTransaction(transactionData); + + // ✅ Optional callback to notify parent + if (onTransactionAdded) { + onTransactionAdded(); + } + setSuccess('Transaction added successfully!'); setFormData({ type: 'income', @@ -72,6 +112,7 @@ const AddTransaction: React.FC = () => { date: '', description: '', }); + setScanMessage(''); } catch (err) { setError('Failed to add transaction'); } finally { @@ -81,84 +122,90 @@ const AddTransaction: React.FC = () => { return (
-
-

Add Transaction

- {error &&
{error}
} - {success &&
{success}
} -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
- -
- {/* Table content here */} +
+

Add Transaction

+ {error &&
{error}
} + {success &&
{success}
} + {scanMessage &&
{scanMessage}
} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
-
- ); }; diff --git a/Frontend/src/components/GenerateRecommendation.tsx b/Frontend/src/components/GenerateRecommendation.tsx index 23cc70b..f33fd8e 100644 --- a/Frontend/src/components/GenerateRecommendation.tsx +++ b/Frontend/src/components/GenerateRecommendation.tsx @@ -7,9 +7,11 @@ export const GenerateRecommendation = () => { const [recommendation, setRecommendation] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [showModal, setShowModal] = useState(false); function handleClick(){ + setShowModal(true); setRecommendation(''); setLoading(true); setError(''); @@ -27,25 +29,37 @@ export const GenerateRecommendation = () => { GenerateRecommendation(); } + const closeModal = () => setShowModal(false); + // if (loading) return ; // if (error) return

{error}

; return (
-
-

New Recommendation!

- - {loading &&

Generating a recommendation...

} - {error &&

{error}

} - {recommendation && ( -
-

Recommends:

-

{JSON.stringify(recommendation, null, 2)}

+
+ {/*

New Recommendation!

*/} + +
+ {showModal && ( +
+
e.stopPropagation()}> + +

Recommendation

+
+ {loading &&

Generating a recommendation...

} + {error &&

{error}

} + {recommendation && ( +
+

{JSON.stringify(recommendation, null, 2)}

+
+ )} +
- )} -
+
+ )} +
diff --git a/Frontend/src/components/IncomeBar.tsx b/Frontend/src/components/IncomeBar.tsx new file mode 100644 index 0000000..04f2810 --- /dev/null +++ b/Frontend/src/components/IncomeBar.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from 'react'; +import { visService } from '../services/card.service'; +import { Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Tooltip, + Legend, + ChartOptions, + TooltipItem, +} from 'chart.js'; +import '../styles/IncomeBar.css'; + +// Register chart.js components +ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); + +// Props to optionally receive filters +interface IncomeBarProps { + queryParams?: { + month?: string; + year?: string; + }; +} + +const IncomeBar: React.FC = ({ queryParams = {} }) => { + const [incomeData, setIncomeData] = useState<{ category: string; total: number }[]>([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchIncomeBySource = async () => { + setLoading(true); + setError(''); + try { + const response = await visService.getIncomeBySource(queryParams); + setIncomeData(response || []); + } catch (err: any) { + console.error(err); + setError('Failed to fetch income data.'); + } finally { + setLoading(false); + } + }; + + fetchIncomeBySource(); + }, [JSON.stringify(queryParams)]); // Refetch when filters change + + const chartData = { + labels: incomeData.map((item) => item.category), + datasets: [ + { + label: 'Income by Source', + data: incomeData.map((item) => item.total), + backgroundColor: '#36A2EB', + borderColor: '#36A2EB', + borderWidth: 1, + }, + ], + }; + + const chartOptions: ChartOptions<'bar'> = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + tooltip: { + callbacks: { + label: (context: TooltipItem<'bar'>) => { + const value = context.raw as number; + return `$${value.toFixed(2)}`; + }, + }, + }, + }, + scales: { + x: { + title: { + display: true, + text: 'Income Source', + }, + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Total Income ($)', + }, + }, + }, + }; + + // Render states + if (loading) return
Loading income data...
; + if (error) return
{error}
; + if (!incomeData.length) return
No income data for the selected filter.
; + + return ( +
+

Income by Source

+
+ +
+
+ ); +}; + +export default IncomeBar; diff --git a/Frontend/src/components/RecommendationList.tsx b/Frontend/src/components/RecommendationList.tsx index 14962f4..1d77284 100644 --- a/Frontend/src/components/RecommendationList.tsx +++ b/Frontend/src/components/RecommendationList.tsx @@ -6,16 +6,12 @@ interface RecommendationListProps { export default function RecommendationList({ recommendations }: RecommendationListProps) { return ( -
-
    +
    {recommendations.map((recommendation) => ( -
  • - {/*

    {recommendation.title}

    */} -

    {recommendation.text}

    -
  • +
    +

    {recommendation.text}

    +
    ))} -
); -} - +} \ No newline at end of file diff --git a/Frontend/src/components/SecurityQuestion.tsx b/Frontend/src/components/SecurityQuestion.tsx new file mode 100644 index 0000000..5ef48c7 --- /dev/null +++ b/Frontend/src/components/SecurityQuestion.tsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:3000', + withCredentials: true, +}); + +const SecurityQuestion = () => { + const [securityQuestion, setSecurityQuestion] = useState(''); + const [answer, setAnswer] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + // Fetch the security question when component mounts + const fetchSecurityQuestion = async () => { + try { + const response = await api.get('/api/auth/security-question'); + setSecurityQuestion(response.data.question); + } catch (err: any) { + setError('Failed to fetch security question'); + console.error(err); + } + }; + + fetchSecurityQuestion(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await api.post('/api/auth/verify-security-question', { answer }); + if (response.data.verified) { + navigate('/dashboard'); + } + } catch (err: any) { + setError('Incorrect answer. Please try again.'); + console.error(err); + } + }; + + return ( +
+
+

Security Verification

+
+
+ +

{securityQuestion}

+
+
+ + setAnswer(e.target.value)} + required + className="input-field" + /> +
+ {error &&

{error}

} + +
+
+
+ ); +}; + +export default SecurityQuestion; \ No newline at end of file diff --git a/Frontend/src/components/ShowBudget.tsx b/Frontend/src/components/ShowBudget.tsx index 628159b..eeec628 100644 --- a/Frontend/src/components/ShowBudget.tsx +++ b/Frontend/src/components/ShowBudget.tsx @@ -1,27 +1,38 @@ import { ChangeEvent, Suspense, useEffect, useState } from 'react'; import LoadingSpinner from './LoadingSpinner'; import ErrorBoundary from './ErrorBoundary'; -import { recommendationService } from '../services/recommendation.service'; import { Budget } from '../types/budget'; import { budgetService } from '../services/budget.service'; -import '../styles/budget.css'; // Adjust the path according to your folder structure +import '../styles/budget.css'; const ShowBudget = () => { const [budgets, setBudgets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 3; // Set the number of budgets per page + const itemsPerPage = 3; - let handleSave = async (id: string | undefined) => { - let budgetUpdated = budgets.filter((bud) => bud._id === id); - const response = await budgetService.updateBudget(id as string, budgetUpdated[0]); - console.log(response); + const handleSave = async (id: string | undefined) => { + if (!id) return; + try { + const budgetUpdated = budgets.find((bud) => bud._id === id); + if (budgetUpdated) { + await budgetService.updateBudget(id, budgetUpdated); + // Optionally add toast notification here + console.log('Budget updated successfully'); + } + } catch (error) { + console.error('Error updating budget:', error); + } }; - let handleCancel = async () => { - const response = await budgetService.getBudgets(); - setBudgets(response as Budget[]); + const handleCancel = async () => { + try { + const response = await budgetService.getBudgets(); + setBudgets(response as Budget[]); + } catch (error) { + console.error('Error fetching budgets:', error); + } }; useEffect(() => { @@ -30,7 +41,7 @@ const ShowBudget = () => { const response = await budgetService.getBudgets(); setBudgets(response as Budget[]); } catch (error) { - setError('Error loading recommendations'); + setError('Error loading budgets'); } finally { setLoading(false); } @@ -42,16 +53,15 @@ const ShowBudget = () => { if (loading) return ; if (error) return

{error}

; - function handleSliderChange(e: ChangeEvent, _id: string | undefined): void { - let temp = budgets; - temp = temp.map((budget) => { - if (budget._id === _id) return { ...budget, limit: +e.target.value }; - else return budget; - }); - setBudgets(temp); - } + const handleSliderChange = (e: ChangeEvent, _id: string | undefined): void => { + if (!_id) return; + setBudgets((prevBudgets) => + prevBudgets.map((budget) => + budget._id === _id ? { ...budget, limit: +e.target.value } : budget + ) + ); + }; - // Pagination Logic: Determine the budgets to show for the current page const indexOfLast = currentPage * itemsPerPage; const indexOfFirst = indexOfLast - itemsPerPage; const currentBudgets = budgets.slice(indexOfFirst, indexOfLast); @@ -59,56 +69,52 @@ const ShowBudget = () => { return (
-

Budgets

+
+

Budgets

+
{currentBudgets.map((budget) => (
-
- image -
-
- Category: {budget?.category?.category} +
+
+ {budget?.category?.category +
+
+

Category: {budget?.category?.category}

+
- Limit: + Limit: ${budget?.limit?.toLocaleString()} handleSliderChange(e, budget._id)} style={{ width: '100%' }} /> - {budget?.limit} - - - + +
- Total Spent: {budget?.total_spent} + Total Spent: ${budget?.total_spent?.toLocaleString()}
))}
- - {/* Pagination Controls */}
- Page {currentPage} + Page {currentPage} of {Math.ceil(budgets.length / itemsPerPage)} +
+ {currentRecommendations.map((recommendation) => ( +
+

{recommendation.text}

+
+ ))} +
+
+ Page {currentPage} of {totalPages} -
- +
); }; -export default function App() { - return ( -
- Error loading recommendations. Please try again later.

}> - }> - - -
-
- ); -} +export default ShowRecommendations; \ No newline at end of file diff --git a/Frontend/src/components/layout.tsx b/Frontend/src/components/layout.tsx index 0e54794..dffd719 100644 --- a/Frontend/src/components/layout.tsx +++ b/Frontend/src/components/layout.tsx @@ -1,6 +1,11 @@ + import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import logoutService from '../services/logout.service'; // Import your logout service +import logoutService from '../services/logout.service'; // Your logout service +import { useEffect } from 'react'; + + + interface LayoutProps { children: React.ReactNode; @@ -8,29 +13,86 @@ interface LayoutProps { const Layout: React.FC = ({ children }) => { const [sidebarVisible, setSidebarVisible] = useState(false); - const [confirmLogout, setConfirmLogout] = useState(false); // State to show confirmation + const [confirmLogout, setConfirmLogout] = useState(false); + const [chatOpen, setChatOpen] = useState(false); + const [chatHistory, setChatHistory] = useState<{ sender: 'user' | 'bot'; text: string }[]>([]); + const [userInput, setUserInput] = useState(''); const navigate = useNavigate(); + // Inside Layout component + useEffect(() => { + const savedHistory = localStorage.getItem('chatHistory'); + if (savedHistory) { + setChatHistory(JSON.parse(savedHistory)); + } + }, []); + + useEffect(() => { + localStorage.setItem('chatHistory', JSON.stringify(chatHistory)); + }, [chatHistory]); + + const toggleSidebar = () => { setSidebarVisible(!sidebarVisible); }; const handleLogoutClick = () => { - setConfirmLogout(true); // Show confirmation dialog when Logout is clicked + setConfirmLogout(true); }; const handleLogoutConfirmation = async (confirm: boolean) => { if (confirm) { try { - // Call the logout service to perform the logout await logoutService(); - // Redirect to login page or home after logout navigate('/login'); } catch (error) { console.error('Logout failed:', error); } } - setConfirmLogout(false); // Close the confirmation dialog + setConfirmLogout(false); + }; + + const toggleChat = () => { + setChatOpen(!chatOpen); + }; + + const sendMessage = async () => { + if (!userInput.trim()) return; + + const userMessage = { sender: "user", text: userInput } as const; + setChatHistory((prev) => [...prev, userMessage]); + + const input = userInput; + setUserInput(''); + + try { + const botReply = await fetchBotResponse(input); + setChatHistory((prev) => [...prev, { sender: 'bot', text: botReply }]); + } catch (error) { + console.error('Error getting bot reply:', error); + setChatHistory((prev) => [ + ...prev, + { sender: 'bot', text: 'Sorry, something went wrong.' }, + ]); + } + }; + + const fetchBotResponse = async (message: string): Promise => { + const response = await fetch('http://localhost:3000/gemini', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message }), // match your backend `message` key + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Failed to fetch bot response.'); + } + + const data = await response.json(); + return data.response; }; return ( @@ -41,7 +103,7 @@ const Layout: React.FC = ({ children }) => {
  • navigate('/dashboard')}>Dashboard
  • navigate('/transactions')}>Transactions
  • navigate('/recommendations')}>Budgets
  • -
  • Logout
  • {/* Logout with confirmation */} +
  • Logout
  • @@ -50,10 +112,10 @@ const Layout: React.FC = ({ children }) => { {children}
    - {/* Menu Icon for Sidebar */} + {/* Menu Icon */} - {/* Confirmation Dialog */} + {/* Logout Confirmation Dialog */} {confirmLogout && (

    Are you sure you want to log out?

    @@ -61,6 +123,44 @@ const Layout: React.FC = ({ children }) => {
    )} + + {/* Floating Chat Button */} + + + {/* Chatbox */} + {chatOpen && ( +
    +
    + PennyWise + +
    +
    + {chatHistory.length === 0 &&
    Hi! I’m PennyWise, your budgeting sidekick. How can I help you today?
    } + {chatHistory.map((msg, idx) => ( +
    + {msg.text} +
    + ))} +
    +
    + setUserInput(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') sendMessage(); }} + /> + +
    +
    + )}
    ); }; diff --git a/Frontend/src/components/login.tsx b/Frontend/src/components/login.tsx index 4f2917d..d8d7f05 100644 --- a/Frontend/src/components/login.tsx +++ b/Frontend/src/components/login.tsx @@ -8,6 +8,8 @@ export const Login = () => { const [password, setPassword] = useState(''); const [name, setName] = useState(''); // For registration const [phone, setPhone] = useState(''); // For registration + const [securityQuestion, setSecurityQuestion] = useState(''); + const [securityAnswer, setSecurityAnswer] = useState(''); const [error, setError] = useState(''); const [message, setMessage] = useState(''); const [isRegistering, setIsRegistering] = useState(false); // To toggle between login and registration @@ -21,7 +23,7 @@ export const Login = () => { setError(''); setMessage(response.message); - navigate('/dashboard'); + navigate('/security-question'); } catch (err: any) { setError(err.response?.data?.error || 'Something went wrong'); } @@ -30,7 +32,14 @@ export const Login = () => { const handleRegister = async (e: React.FormEvent) => { e.preventDefault(); try { - const params = { name, email, phone, password }; + const params = { + name, + email, + phone, + password, + securityQuestion, + securityAnswer + }; const response = await registerService.Register(params); setError(''); @@ -70,6 +79,29 @@ export const Login = () => { className="input-field" /> +
    + + setSecurityQuestion(e.target.value)} + required + className="input-field" + placeholder="e.g., What was your first pet's name?" + /> +
    +
    + + setSecurityAnswer(e.target.value)} + required + className="input-field" + /> +
    )}
    diff --git a/Frontend/src/pages/BudgetPage.tsx b/Frontend/src/pages/BudgetPage.tsx index ee13f72..c0a4648 100644 --- a/Frontend/src/pages/BudgetPage.tsx +++ b/Frontend/src/pages/BudgetPage.tsx @@ -1,32 +1,30 @@ import ShowRecommendations from '../components/ShowRecommendation'; import GenerateRecommendation from '../components/GenerateRecommendation'; import ShowBudget from '../components/ShowBudget'; -import '../styles/budget.css' -import '../components/layout' -import '../styles/layout.css' +import '../styles/budget.css'; import Layout from '../components/layout'; + const RecommendationPage = () => { - return ( -
    - -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    -
    - ) -} + +
    +
    + +
    +
    +
    +

    Your Recommendations

    + +
    +
    +
    + +
    +
    +
    +
    +
    + ); +}; -export default RecommendationPage; +export default RecommendationPage; \ No newline at end of file diff --git a/Frontend/src/pages/DashboardPage.tsx b/Frontend/src/pages/DashboardPage.tsx index 18421e4..abee349 100644 --- a/Frontend/src/pages/DashboardPage.tsx +++ b/Frontend/src/pages/DashboardPage.tsx @@ -6,9 +6,20 @@ import '../styles/dashbored.css'; import { visService } from "../services/card.service"; import { Transaction } from '../types/transaction'; import { transactionsService } from '../services/transactions.service'; +import IncomeBar from '../components/IncomeBar'; -import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, PointElement, LineElement } from 'chart.js'; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, + CategoryScale, + LinearScale, + PointElement, + LineElement, +} from 'chart.js'; import { Pie, Line } from 'react-chartjs-2'; + ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, PointElement, LineElement); const DashboardPage: React.FC = () => { @@ -16,48 +27,57 @@ const DashboardPage: React.FC = () => { const [error, setError] = useState(''); const [showUpdateProfileForm, setShowUpdateProfileForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); + const [updateFormData, setUpdateFormData] = useState({ name: '', email: '', phone: '' }); + const [passwordData, setPasswordData] = useState({ email: '', password: '' }); const [totalIncomeAndExpenses, setTotalIncomeAndExpenses] = useState(null); const [transactions, setTransactions] = useState([]); const [chartData, setChartData] = useState(null); + const [isFilterOn, setIsFilterOn] = useState(false); + const [tempMonth, setTempMonth] = useState(''); + const [tempYear, setTempYear] = useState('2025'); + const [selectedMonth, setSelectedMonth] = useState(''); + const [selectedYear, setSelectedYear] = useState('2025'); + + const queryParams = isFilterOn ? { + ...(selectedMonth && { month: selectedMonth }), + ...(selectedYear && { year: selectedYear }), + } : {}; + useEffect(() => { const fetchTransactions = async () => { try { - const filters = { - date: "2023-12-22", - category: "Groceries", - }; - const filteredTransactions = await transactionsService.getAllTransactions(filters); + const filteredTransactions = await transactionsService.getAllTransactions({ ...queryParams }); setTransactions(filteredTransactions); - const labels = filteredTransactions.map((transaction: Transaction) => transaction.date); - const amounts = filteredTransactions.map((transaction: Transaction) => transaction.amount); + const labels = filteredTransactions.map((t: Transaction) => t.date); + const amounts = filteredTransactions.map((t: Transaction) => t.amount); setChartData({ labels, datasets: [ { - label: "Transaction Amounts", + label: 'Transaction Amounts', data: amounts, - borderColor: "rgba(75, 192, 192, 1)", - backgroundColor: "rgba(75, 192, 192, 0.2)", + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', borderWidth: 2, tension: 0.4, }, ], }); - } catch (error) { - console.error("Error fetching transactions:", error); + } catch (err) { + console.error("Error fetching transactions:", err); } }; fetchTransactions(); - }, []); + }, [selectedMonth, selectedYear, isFilterOn]); useEffect(() => { const fetchTotalIncomeAndExpenses = async () => { try { - const response = await visService.getTotalIncomeAndExpenses(); + const response = await visService.getTotalIncomeAndExpenses({ ...queryParams }); setTotalIncomeAndExpenses(response); } catch (error) { console.error("Error fetching total income and expenses:", error); @@ -65,37 +85,15 @@ const DashboardPage: React.FC = () => { }; fetchTotalIncomeAndExpenses(); - }, []); - - const [profileData, setProfileData] = useState({ - name: '', - email: '', - phone: '', - }); - const toggleUpdateProfileForm = () => setShowUpdateProfileForm((prev) => !prev); - const toggleChangePasswordForm = () => setShowChangePasswordForm((prev) => !prev); - - const [passwordData, setPasswordData] = useState({ - email: '', - password: '', - }); - - const pieData = totalIncomeAndExpenses && { - labels: ['Income', 'Expenses'], - datasets: [ - { - data: [totalIncomeAndExpenses.income, totalIncomeAndExpenses.expenses], - backgroundColor: ['#36A2EB', '#FF6384'], - hoverBackgroundColor: ['#36A2EB', '#FF6384'], - }, - ], - }; + }, [selectedMonth, selectedYear, isFilterOn]); useEffect(() => { const fetchProfile = async () => { try { const data = await profileService.getProfile(); setProfile(data.profile); + setUpdateFormData({ name: data.profile.name, email: data.profile.email, phone: data.profile.phone }); + setPasswordData({ email: data.profile.email, password: '' }); } catch (err: any) { setError(err.message || 'Failed to load profile'); } @@ -112,27 +110,37 @@ const DashboardPage: React.FC = () => { useEffect(() => { const fetchCardData = async () => { try { - const maxExpenseData = await visService.getMaxExpense(); - getMaxExpense(maxExpenseData); - - const minExpenseData = await visService.getMinExpense(); - getMinExpense(minExpenseData); - - const balanceData = await visService.getBalance(); - getBalance(balanceData); - - const spent12Months = await visService.getSpentLast12Months(); - getSpentLast12Months(spent12Months); + const max = await visService.getMaxExpense({ ...queryParams }); + const min = await visService.getMinExpense({ ...queryParams }); + const bal = await visService.getBalance({ ...queryParams }); + const last12 = await visService.getSpentLast12Months(); + const last30 = await visService.getSpentLast30Days(); - const spent30Days = await visService.getSpentLast30Days(); - getSpentLast30Days(spent30Days); + getMaxExpense(max); + getMinExpense(min); + getBalance(bal); + getSpentLast12Months(last12); + getSpentLast30Days(last30); } catch (error) { console.error("Error fetching card data:", error); } }; fetchCardData(); - }, []); + }, [selectedMonth, selectedYear, isFilterOn]); + + const pieData = totalIncomeAndExpenses && typeof totalIncomeAndExpenses.income === 'number' && typeof totalIncomeAndExpenses.expenses === 'number' + ? { + labels: ['Income', 'Expenses'], + datasets: [ + { + data: [totalIncomeAndExpenses.income, totalIncomeAndExpenses.expenses], + backgroundColor: ['#36A2EB', '#FF6384'], + hoverBackgroundColor: ['#36A2EB', '#FF6384'], + }, + ], + } + : null; return ( @@ -145,23 +153,83 @@ const DashboardPage: React.FC = () => {

    Hello, {profile.name}!

    Email: {profile.email}

    Phone: {profile.phone}

    - - + + {showUpdateProfileForm && ( +
    { + e.preventDefault(); + try { + await profileService.updateProfile(updateFormData); + alert('Profile updated successfully'); + setShowUpdateProfileForm(false); + } catch (err: any) { + alert(err); + } + }}> + setUpdateFormData({ ...updateFormData, name: e.target.value })} placeholder="Name" /> + setUpdateFormData({ ...updateFormData, email: e.target.value })} placeholder="Email" /> + setUpdateFormData({ ...updateFormData, phone: e.target.value })} placeholder="Phone" /> + +
    + )} + + {showChangePasswordForm && ( +
    { + e.preventDefault(); + try { + await profileService.changePassword(passwordData); + alert('Password changed successfully'); + setPasswordData({ ...passwordData, password: '' }); + setShowChangePasswordForm(false); + } catch (err: any) { + alert(err); + } + }}> + setPasswordData({ ...passwordData, email: e.target.value })} placeholder="Email" /> + setPasswordData({ ...passwordData, password: e.target.value })} placeholder="New Password" /> + +
    + )}
    - ) : ( -

    Loading profile...

    - )} + ) :

    Loading profile...

    }

    Welcome to Your Dashboard

    -

    Here is a summary of your financial activity.

    +
    + + {isFilterOn ? 'Filter: ON' : 'Filter: OFF'} + {isFilterOn && ( + <> +    + +    + + + + )} +
    @@ -169,37 +237,26 @@ const DashboardPage: React.FC = () => {

    Income vs Expenses

    - {totalIncomeAndExpenses ? :

    Loading Chart...

    } + {pieData ? :

    Loading Chart...

    } +
    +
    +
    -

    Your Financial Highlights

    -

    Max Expense

    {maxExpense ? `$${maxExpense}` : "Loading..."}

    -

    Min Expense

    {minExpense ? `$${minExpense}` : "Loading..."}

    -

    Balance

    {balance ? `$${balance}` : "Loading..."}

    -

    Spent (Last 12 Months)

    {spentLast12Months ? `$${spentLast12Months}` : "Loading..."}

    -

    Spent (Last 30 Days)

    {spentLast30Days ? `$${spentLast30Days}` : "Loading..."}

    +

    Max Expense

    {maxExpense !== null ? `$${maxExpense}` : "Loading..."}

    +

    Min Expense

    {minExpense !== null ? `$${minExpense}` : "Loading..."}

    +

    Balance

    {balance !== null ? `$${balance}` : "Loading..."}

    +

    Spent (Last 12 Months)

    {spentLast12Months !== null ? `$${spentLast12Months}` : "Loading..."}

    +

    Spent (Last 30 Days)

    {spentLast30Days !== null ? `$${spentLast30Days}` : "Loading..."}

    -

    Transaction Line Graph

    {chartData ? ( - - ) : ( -

    Loading Chart...

    - )} + + ) :

    Loading Chart...

    }
    @@ -208,4 +265,4 @@ const DashboardPage: React.FC = () => { ); }; -export default DashboardPage; +export default DashboardPage; \ No newline at end of file diff --git a/Frontend/src/pages/TransactionsPage.tsx b/Frontend/src/pages/TransactionsPage.tsx index bdfbe5c..99f972b 100644 --- a/Frontend/src/pages/TransactionsPage.tsx +++ b/Frontend/src/pages/TransactionsPage.tsx @@ -1,28 +1,24 @@ import React from 'react'; import TransactionList from '../components/TransactionList'; // Adjust the import path based on your folder structure import AddTransaction from '../components/AddTransaction'; - -import '../styles/layout.css' import Layout from '../components/layout'; - import "../styles/TransactionPage.css" + const TransactionPage: React.FC = () => { return ( -
    - +
    -

    Transaction Management

    -
    -
    - -
    -
    - +

    Transaction Management

    +
    +
    +
    +
    +
    - -
    -
    +
    +
    +
    ); }; diff --git a/Frontend/src/services/card.service.ts b/Frontend/src/services/card.service.ts index 6345b38..a3f1110 100644 --- a/Frontend/src/services/card.service.ts +++ b/Frontend/src/services/card.service.ts @@ -9,9 +9,9 @@ const api = axios.create({ // Service for Visualization data export const visService = { // Fetch total income and expenses for the pie chart - async getTotalIncomeAndExpenses() { - const incomeResponse = await api.get('/total-income'); - const expensesResponse = await api.get('/total-expenses'); + async getTotalIncomeAndExpenses(queryParams: Record = {}) { + const incomeResponse = await api.get('/total-income', { params: queryParams }); + const expensesResponse = await api.get('/total-expenses', { params: queryParams }); return { income: incomeResponse.data.income, expenses: expensesResponse.data.expenses, @@ -52,4 +52,13 @@ async getMaxExpense( queryParams: Record = {}): Promise { console.log(response); return response.data.maxExpense; }, + +//updates + +// Fetch income data by source +async getIncomeBySource(queryParams: Record = {}) { + const response = await api.get('/income-by-source', { params: queryParams }); + console.log(response); + return response.data.incomeSources; // Assuming incomeSources is an array of { category, total } +}, }; diff --git a/Frontend/src/services/reg.services.ts b/Frontend/src/services/reg.services.ts index 2438574..fed1297 100644 --- a/Frontend/src/services/reg.services.ts +++ b/Frontend/src/services/reg.services.ts @@ -6,7 +6,14 @@ const api = axios.create({ }); export const registerService = { - async Register(data: { name: string; email: string; phone: string; password: string }) { + async Register(data: { + name: string; + email: string; + phone: string; + password: string; + securityQuestion: string; + securityAnswer: string; + }) { // Send user details for registration const response = await api.post('api/auth/reg/', data, { withCredentials: true }); return response.data; diff --git a/Frontend/src/services/transactions.service.ts b/Frontend/src/services/transactions.service.ts index 6bff92e..12246dc 100644 --- a/Frontend/src/services/transactions.service.ts +++ b/Frontend/src/services/transactions.service.ts @@ -1,18 +1,20 @@ +// services/transactions.service.ts + import axios from 'axios'; -import { Transaction, TransactionFilters } from '../types/transaction'; -// import { API_BASE_URL } from '../config/api'; +import { Transaction, TransactionFilters, CreateTransactionDTO } from '../types/transaction'; const api = axios.create({ baseURL: 'http://localhost:3000', - withCredentials: true, // This is important for sending cookies with requests + withCredentials: true, }); export const transactionsService = { - - async getAllTransactions(filters?: TransactionFilters): Promise { + async getAllTransactions(filters?: TransactionFilters & { month?: string; year?: string }): Promise { const params = new URLSearchParams(); if (filters?.date) params.append('date', filters.date); if (filters?.category) params.append('category', filters.category); + if (filters?.month) params.append('month', filters.month); + if (filters?.year) params.append('year', filters.year); const response = await api.get('/transactions', { params }); return response.data; @@ -23,7 +25,7 @@ export const transactionsService = { return response.data; }, - async addTransaction(transaction: Omit): Promise { + async addTransaction(transaction: CreateTransactionDTO): Promise { const response = await api.post('/transactions', transaction); return response.data; }, @@ -31,6 +33,17 @@ export const transactionsService = { async deleteTransaction(id: string): Promise { await api.delete(`/transactions/${id}`); }, + + async scanReceipt(file: File): Promise<{ amount: number; date: string }> { + const formData = new FormData(); + formData.append('receipt', file); + + const response = await api.post('/transactions/scan', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + + return response.data; + }, }; diff --git a/Frontend/src/styles/IncomeBar.css b/Frontend/src/styles/IncomeBar.css new file mode 100644 index 0000000..c2271ae --- /dev/null +++ b/Frontend/src/styles/IncomeBar.css @@ -0,0 +1,57 @@ +Container for the bar chart +.income-bar-container { + width: 100%; + padding: 20px; + background: #f9f9f9; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin: 20px 0; + text-align: center; + } + + .income-bar-container h2 { + font-size: 1.5rem; + color: #333; + margin-bottom: 20px; + font-family: 'Arial', sans-serif; + font-weight: 600; + } + + /* The bar chart styling */ + .bar-chart { + position: relative; + width: 100%; + max-width: 800px; + margin: 0 auto; + } + + .chartjs-render-monitor { + background: linear-gradient(45deg, #ff66b2, #9b59b6); /* Pink to Purple Gradient */ + border-radius: 8px; + padding: 20px; + } + + .chart-container { + position: relative; + } + + /* Tooltip styling */ + .chartjs-tooltip { + background-color: rgba(0, 0, 0, 0.8); + color: white; + font-size: 14px; + padding: 10px; + border-radius: 5px; + } + + /* Responsive behavior */ + @media (max-width: 768px) { + .income-bar-container { + padding: 15px; + } + + .bar-chart { + width: 90%; + } + } + \ No newline at end of file diff --git a/Frontend/src/styles/TransactionPage.css b/Frontend/src/styles/TransactionPage.css index 90c5f7e..3bbfc0f 100644 --- a/Frontend/src/styles/TransactionPage.css +++ b/Frontend/src/styles/TransactionPage.css @@ -6,6 +6,7 @@ } html, body { + height: 100%; overflow-y: auto; font-family: Arial, sans-serif; } @@ -13,48 +14,67 @@ html, body { /* Transaction Page Styles */ .transaction-page { width: 100%; - padding: 0%; - min-height: 100vh; + padding: 20px; + min-height: calc(100vh - 60px); /* Account for any headers/layout spacing */ + background-color: var(--bg-page); + overflow-y: auto; } .transaction-page h1 { text-align: center; - color: #4b2c7f; /* Soft pastel purple */ + color: #4b2c7f; margin-bottom: 30px; + font-size: 32px; + font-weight: 700; + position: sticky; + top: 0; + background-color: var(--bg-page); + padding: 20px 0; + z-index: 10; } .transaction-container { - display: flex; justify-content: space-between; - gap: 30px; /* Space between Add and List */ + gap: 30px; + max-width: 1280px; + margin: 0 auto; + padding: 20px; + height: calc(100vh - 150px); /* Adjust based on header height */ + overflow-y: auto; } .add { - width: 48%; /* Adjust width to fit both sections side by side */ + flex: 1; + min-width: 300px; + max-width: 500px; + height: fit-content; } .list { - width: 48%; + flex: 1.5; + min-width: 600px; + max-height: 100%; + overflow-y: auto; } /* Add Transaction Styles */ .add-transaction-container { display: flex; justify-content: center; + position: sticky; + top: 0; } .add-transaction { - background: rgba(255, 255, 255, 0.85); /* Semi-transparent white */ + background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); padding: 30px; width: 100%; - max-width: 500px; color: #333; transition: transform 0.3s ease, box-shadow 0.3s ease; animation: popIn 0.5s ease; - overflow-y: auto; } /* Pop In Animation */ @@ -63,8 +83,6 @@ html, body { 100% { transform: scale(1); opacity: 1; } } - - .form-header { text-align: center; color: #4b2c7f; @@ -74,7 +92,8 @@ html, body { /* Transaction List Styles */ .transactionlist { - width: 650px; + width: 100%; + max-width: 650px; margin: 0 auto; } @@ -83,12 +102,15 @@ html, body { box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2); } .transaction-list-container { - background: rgba(255, 255, 255, 0.85); + background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); - padding: 30px 10px; + padding: 30px 10px; animation: popIn 0.5s ease; - + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; } .transaction-list-header { @@ -97,6 +119,11 @@ html, body { font-size: 28px; margin-bottom: 20px; border-radius: 100px; + position: sticky; + top: 0; + background-color: rgba(255, 255, 255, 0.95); + padding: 15px 0; + z-index: 5; } .filters-container { @@ -104,6 +131,11 @@ html, body { display: flex; justify-content: space-between; gap: 15px; + position: sticky; + top: 70px; + background-color: rgba(255, 255, 255, 0.95); + padding: 10px 0; + z-index: 5; } .filters-select { @@ -121,21 +153,26 @@ html, body { } .transaction-table-container { - margin-top: 30px; + margin-top: 20px; + overflow-y: auto; + flex: 1; } .transaction-table { width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; } .transaction-table-header th { background-color: #6c63ff; color: white; - padding: 20px; + padding: 15px; text-align: left; font-size: 16px; - + position: sticky; + top: 0; + z-index: 4; } .transaction-table-body tr { @@ -257,3 +294,76 @@ html, body { font-size: 16px; color: #333; } + +/* Custom Scrollbar Styling */ +.transaction-container::-webkit-scrollbar, +.list::-webkit-scrollbar, +.transaction-table-container::-webkit-scrollbar { + width: 8px; +} + +.transaction-container::-webkit-scrollbar-track, +.list::-webkit-scrollbar-track, +.transaction-table-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.transaction-container::-webkit-scrollbar-thumb, +.list::-webkit-scrollbar-thumb, +.transaction-table-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.transaction-container::-webkit-scrollbar-thumb:hover, +.list::-webkit-scrollbar-thumb:hover, +.transaction-table-container::-webkit-scrollbar-thumb:hover { + background: #666; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .transaction-container { + flex-direction: column; + align-items: center; + height: auto; + } + + .add, .list { + width: 100%; + max-width: 800px; + min-width: auto; + } + + .list { + margin-top: 30px; + height: 600px; /* Fixed height for scrollable list on mobile */ + } +} + +@media (max-width: 768px) { + .transaction-page { + padding: 10px; + } + + .transaction-container { + padding: 10px; + } + + .transaction-page h1 { + font-size: 24px; + margin-bottom: 20px; + padding: 10px 0; + } + + .filters-container { + flex-direction: column; + gap: 10px; + position: static; + } + + .transaction-table-container { + overflow-x: auto; + } +} diff --git a/Frontend/src/styles/budget.css b/Frontend/src/styles/budget.css index 350f838..de42819 100644 --- a/Frontend/src/styles/budget.css +++ b/Frontend/src/styles/budget.css @@ -1,209 +1,415 @@ -/* Global Styles */ +/* =========================== + budget.css — Dashboard‐Style UI + =========================== */ + +/* ---------- Root Variables ---------- */ +:root { + --bg-page: #f7f5ff; /* Light purple page background */ + --bg-card: #ffffff; /* White for cards/containers */ + --text-default: #333333; /* Dark gray text */ + --text-muted: #6a4d9e; /* Soft purple for secondary text */ + --primary: #4b2c7f; /* Deep purple accent */ + --primary-hover: #3a2061; /* Darker purple on hover */ + --accent: rgba(255, 105, 180, 0.7); /* Soft pink for dividers / accents */ + --shadow-light: 0 4px 12px rgba(0, 0, 0, 0.08); + --radius: 12px; /* Slightly larger radius for modern look */ + --transition: all 0.3s ease; /* Smooth transitions */ +} + +/* ---------- Reset & Scroll ---------- */ * { margin: 0; padding: 0; box-sizing: border-box; - font-family: 'Arial', sans-serif; + font-family: 'Inter', Arial, sans-serif; /* Use Inter for modern typography */ } -/* Scope the styles with .dashboard-page */ -html, body, .budget { - overflow-y: auto; /* Enable scrolling for the body */ +html, body { + height: 100%; /* Ensure full height for scrolling */ + background-color: var(--bg-page); + color: var(--text-default); + overflow-x: hidden; /* Prevent horizontal scroll */ } -/* Container for the entire page */ -.budget-container { +/* ---------- Budget Wrapper & Centering ---------- */ +.budget { display: flex; flex-direction: column; - padding: 20px; - margin: auto; - max-width: 1200px; - background-color: #fff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 10px; - overflow-y: auto; -} - -/* Top section (for ShowBudget) */ -.top-section { - margin-bottom: 20px; - overflow-y: auto; + align-items: center; + padding: 30px; /* Increased padding */ + background-color: var(--bg-page); + min-height: 60vh; /* Allow content to expand beyond viewport */ } -/* Bottom section (container for left and right sections) */ -.bottom-section { - display: flex; - justify-content: space-between; - gap: 20px; /* Adds space between left and right sections */ - overflow-y: auto; +/* ---------- Main Container (Centered) ---------- */ +.budget-container { + background-color: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow-light); + width: 90%; /* Slightly wider for better content fit */ + max-width: 1280px; /* Larger max-width for modern screens */ + padding: 30px; /* More padding for breathing room */ + margin: 30px auto; + position: relative; + height: calc(100vh - 60px); /* Full viewport height minus margins */ + overflow-y: auto; /* Enable vertical scrolling */ } -/* Left section (for GenerateRecommendation) */ -.left-section { - flex: 1; - background-color: #f9f9f9; - border-radius: 8px; - padding: 20px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - overflow-y: auto; +/* ---------- Top Section (Budgets Grid) ---------- */ +.top-section { + width: 100%; + margin-bottom: 10px; /* More spacing */ } -/* Right section (for ShowRecommendations) */ -.right-section { - flex: 1; - background-color: #f9f9f9; - border-radius: 8px; - padding: 20px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - overflow-y: auto; +/* Title for Budgets */ +.budget-title h1 { + font-size: 32px; /* Larger title */ + font-weight: 700; + color: var(--primary); + margin-bottom: 20px; + text-align: center; } -/* Budget Styles */ -.budget { - padding: 10px; +/* ---------- Grid of Budget Cards (Three Columns) ---------- */ +.budget-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 30px; /* More spacing between cards */ } -.budget-container { +.budget-item { + background-color: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow-light); + padding: 24px; display: flex; flex-direction: column; - padding: 10px; - margin: auto; - max-width: 1200px; - background-color: #fff; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 10px; - overflow-y: auto; + gap: 16px; + transition: var(--transition); } -.budget-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 10px; +.budget-item:hover { + transform: translateY(-6px); /* Slightly more lift on hover */ + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); } -.budget-item { - background-color: #f9f9f9; - border-radius: 8px; - padding: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +/* Budget Card Header */ +.budget-header { display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; + align-items: center; + gap: 12px; } -.budget-category, -.budget-value { - font-size: 1rem; +.budget-image img { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; /* Ensure images fit nicely */ +} + +.budget-category p { + font-size: 1.2rem; + font-weight: 600; + color: var(--primary); } +/* Budget Limits & Controls */ .budget-value { - display: block; + display: flex; align-items: center; - justify-content: space-between; - width: 100%; + gap: 16px; + flex-wrap: wrap; } -.budget-value input { +.budget-value strong { + flex-shrink: 0; + font-size: 1.1rem; + color: var(--text-default); +} + +.budget-value input[type="range"] { flex-grow: 1; - margin-right: 15px; - padding: 8px; + -webkit-appearance: none; + height: 8px; border-radius: 4px; - border: 1px solid #ddd; + background: #e0e0e0; + outline: none; + accent-color: var(--primary); + transition: var(--transition); } -.budget button{ - background-color: #4caf50; - margin-top: 20px; - border-radius: 5px; - height: 60px; - border: none; - color: white; - padding: 8px 15px; - font-size: large ; - + +.budget-value input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; } .budget-value button { - padding: 8px 15px; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); /* Gradient button */ + color: #fff; border: none; - background-color: #4caf50; - color: white; - border-radius: 5px; + border-radius: 8px; + padding: 10px 20px; + font-size: 0.95rem; cursor: pointer; - margin: 10px ; - width :70px; - font-size: small; - height: auto; + transition: var(--transition); } .budget-value button:hover { - background-color: #45a049; + background: var(--primary-hover); + transform: scale(1.05); /* Subtle scale effect */ } -.budget-image img { - border-radius: 50%; - margin-right: 15px; +/* Total Spent Line */ +.budget-value + .budget-value { + margin-top: 12px; + font-size: 1rem; + color: var(--text-muted); } +/* ---------- Pagination (Centered) ---------- */ .pagination { display: flex; justify-content: center; - gap: 10px; - margin-top: 20px; + align-items: center; + gap: 20px; + margin: 30px 0; } .pagination button { - padding: 5px 10px; - background-color: #007bff; - color: white; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + color: #fff; border: none; - border-radius: 5px; + border-radius: 8px; + padding: 10px 16px; + font-size: 0.95rem; cursor: pointer; - transition: background-color 0.3s ease; - margin-top: 0; - font-size: small; - height: auto; + transition: var(--transition); +} + +.pagination button:hover:not(:disabled) { + background: var(--primary-hover); + transform: scale(1.05); } .pagination button:disabled { - background-color: #d6d6d6; + background-color: #ccc; + cursor: not-allowed; } -.pagination button:hover { - background-color: #0056b3; +.pagination span { + color: var(--text-muted); + font-size: 1rem; + font-weight: 500; } +/* ---------- Bottom Section (Recommendations) ---------- */ +.bottom-section { + display: flex; + justify-content: space-between; + gap: 30px; + margin-top: 30px; + height: calc(100% - 50px); /* Take remaining height */ + overflow-y: auto; /* Enable vertical scrolling */ +} -/* Container for the recommendations section */ -.recommendation-container { - padding: 20px; - background-color: #fff; - border-radius: 10px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +/* Left Section (Recommendations List) */ +.left-section { + flex: 2; + overflow-y: auto; /* Enable vertical scrolling */ + max-height: 100%; /* Take full height of parent */ } -/* Title of the recommendations section */ .recommendation-title { - font-size: 1.5rem; - color: #333; - margin-bottom: 20px; + font-size: 28px; + font-weight: 700; + color: var(--primary); + margin-bottom: 24px; + text-align: left; } -/* List of recommendations */ .recommendation-list { display: flex; flex-direction: column; - gap: 15px; + gap: 20px; } -/* Individual recommendation item */ .recommendation-item { - background-color: #f9f9f9; - padding: 15px; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-color: #fafafa; + border-radius: var(--radius); + padding: 20px; + transition: var(--transition); + font-size: 1.1rem; + color: var(--text-default); } .recommendation-item:hover { - background-color: #f1f1f1; + background-color: #f0f0f5; + transform: translateX(4px); /* Subtle slide effect */ } + +/* Right Section (Generate Recommendation Button) */ +.right-section { + flex: 1; + background-color: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow-light); + padding: 24px; + height: fit-content; /* Adjust height to content */ + position: sticky; + top: 30px; /* Stick to top with some padding */ +} + +.generate-rec { + margin-top: 20px; +} + +.recommendation-button { + width: 100%; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + color: #fff; + border: none; + border-radius: 8px; + padding: 14px 24px; + font-size: 1.1rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); +} + +.recommendation-button:hover { + background: var(--primary-hover); + transform: scale(1.05); +} + +/* ---------- Modal Overlay & Popup ---------- */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-popup { + background-color: var(--bg-card); + padding: 2rem; + border-radius: var(--radius); + width: 90%; + max-width: 700px; + max-height: 85vh; + overflow-y: auto; + box-shadow: 0 0 12px rgba(0, 0, 0, 0.2); + position: relative; +} + +.modal-popup pre { + white-space: pre-wrap; + word-wrap: break-word; + margin-top: 1.2rem; + font-size: 1rem; +} + +.model-text { + padding: 12px; + margin-top: 12px; + font-size: 1.1rem; + color: var(--text-default); + line-height: 1.6; +} + +.close-btn { + position: absolute; + top: 12px; + right: 18px; + background: transparent; + border: none; + font-size: 1.8rem; + cursor: pointer; + color: var(--text-muted); + transition: var(--transition); +} + +.close-btn:hover { + color: var(--primary); +} + +/* ---------- Recommendation List Table (Bullet Style) ---------- */ +.recommendations-table { + width: 100%; + border-collapse: collapse; + margin-top: 12px; +} + +.recommendations-table td { + padding: 12px; + border-bottom: 1px solid #e0e0e0; + font-size: 1.1rem; + color: var(--text-default); +} + +.recommendations-table td::before { + content: "• "; + color: var(--primary); + font-size: 1.3em; + font-weight: bold; + margin-right: 6px; +} + +.recommendation-text { + font-size: 1rem; + color: var(--text-default); /* Use default text for better readability */ + text-align: left; + line-height: 1.6; +} + +/* ---------- Utilities ---------- */ +.p-4 { + align-self: center; + margin-bottom: 12px; + text-align: center; +} + +.budget-title { + padding: 12px; + margin-bottom: 12px; + text-align: center; +} + +/* ---------- Responsive Fallbacks ---------- */ +@media (max-width: 1024px) { + .budget-grid { + grid-template-columns: repeat(2, 1fr); + } + .bottom-section { + flex-direction: column; + gap: 30px; + } + .budget-container { + width: 95%; + padding: 20px; + } +} + +@media (max-width: 768px) { + .budget-grid { + grid-template-columns: 1fr; + } + .budget-container { + padding: 16px; + } + .budget-title h1 { + font-size: 28px; + } + .recommendation-title { + font-size: 24px; + } +} \ No newline at end of file diff --git a/Frontend/src/styles/dashbored.css b/Frontend/src/styles/dashbored.css index d45e2eb..01d9a60 100644 --- a/Frontend/src/styles/dashbored.css +++ b/Frontend/src/styles/dashbored.css @@ -1,15 +1,23 @@ -/* Scope the styles with .dashboard-page */ +/* ====================================================== + GLOBAL / RESET + ====================================================== */ html, body { - - overflow-y: auto; /* Enable scrolling for the body */ + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; + /* Let grow and scroll if needed */ } + .dashboard-page { - height: 100%; margin: 0; font-family: 'Arial', sans-serif; - overflow-y: auto; /* Enable scrolling for the body */ - } - .confirmation-dialog { + /* No fixed height or overflow here—container will scroll */ +} + +/* ====================================================== + CONFIRMATION DIALOG (UNCHANGED) + ====================================================== */ +.confirmation-dialog { position: fixed; top: 50%; left: 50%; @@ -19,134 +27,318 @@ html, body { border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - } - - .confirmation-dialog button { +} + +.confirmation-dialog button { margin: 10px; padding: 10px 20px; cursor: pointer; - } - - .confirmation-dialog button:first-child { +} + +.confirmation-dialog button:first-child { background-color: green; color: white; - } - - .confirmation-dialog button:last-child { +} + +.confirmation-dialog button:last-child { background-color: red; color: white; - } - - .dashboard-container { +} + +/* ====================================================== + DASHBOARD CONTAINER (CENTERED + SCROLLABLE) + ====================================================== */ +.dashboard-container { + /* Appearance */ padding: 20px; background-color: #f7f5ff; /* Light purple background */ border-radius: 12px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - margin-top: 20px; - width: 80%; + + /* Center horizontally and fill viewport vertically */ position: absolute; top: 0; - left: 150px; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 80%; /* Adjust as needed (e.g. 75%, 90%) */ + max-width: 1200px; /* Caps the width on large screens */ + + /* THIS enables internal vertical scrolling */ overflow-y: auto; - } - - .dashboard-page .profile-section { + overflow-x: hidden; +} + +/* On very narrow screens, adjust left/right margins to avoid overflow */ +@media (max-width: 768px) { + .dashboard-container { + width: 95%; + transform: translateX(-47.5%); /* (100% − 95%) ÷ 2 = 2.5% shift */ + } +} + +/* ====================================================== + PROFILE SECTION (HEADER) + ====================================================== */ +.dashboard-page .profile-section { background-color: #fff; - padding: 20px; + padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - margin-bottom: 20px; - } - - .dashboard-page .profile-details { - font-family: 'Arial', sans-serif; - color: #333; - } - - .dashboard-page .greeting { - font-size: 30px; + margin-bottom: 25px; +} + +.dashboard-page .profile-details h2 { + font-size: 28px; font-weight: bold; color: #4b2c7f; /* Purple color */ - } - - .dashboard-page .profile-item { + margin: 0 0 10px; +} + +.dashboard-page .profile-item, +.dashboard-page .profile-details p { font-size: 16px; margin: 5px 0; color: #6a4d9e; - } - - .dashboard-page .error-message { +} + +.dashboard-page .error-message { color: #ff4d4d; font-size: 14px; margin-bottom: 10px; - } - - .dashboard-page .divider-line { +} + +.dashboard-page .profile-details button { + background-color: #4b2c7f; + color: #fff; + padding: 8px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + margin-right: 10px; + margin-top: 10px; +} + +.dashboard-page .profile-details button:hover { + background-color: #3a2061; +} + +/* ====================================================== + DIVIDER LINE + ====================================================== */ +.dashboard-page .divider-line { border: 0; height: 2px; background-color: rgba(255, 105, 180, 0.7); /* Soft pink */ - margin: 20px 0; - } - - .dashboard-page .dashboard-content { + margin: 30px 0; +} + +/* ====================================================== + DASHBOARD CONTENT (FILTER + HEADLINE) + ====================================================== */ +.dashboard-page .dashboard-content { background-color: #fff; - padding: 20px; + padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - } - - .dashboard-page .dashboard-heading { + margin-bottom: 30px; +} + +.dashboard-page .dashboard-heading { font-size: 28px; - color: #4b2c7f; /* Purple color */ - } - - .dashboard-page .dashboard-description { + color: #4b2c7f; + margin: 0 0 15px; +} + +.dashboard-page .dashboard-description { font-size: 16px; + color: #6a4d9e; +} + +.dashboard-page .filter-controls { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.dashboard-page .filter-controls .switch { + margin-right: 10px; +} + +.dashboard-page .filter-controls span { + font-size: 16px; + color: #333; +} + +/* ====================================================== + VISUALIZATION SECTION + ====================================================== */ +.visualization-section { + padding: 10px 0; +} + +.visualization-section h2 { + margin: 0 0 20px; + text-align: center; + font-size: 26px; + color: #4b2c7f; +} + +/* Container for pie chart, bar chart, and cards */ +.pie-and-cards-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; +} + +/* ====================================================== + PIE CHART AREA + ====================================================== */ +.pie { + flex: 1 1 40%; /* Roughly 40% of the row */ + min-width: 280px; /* Don’t shrink smaller than 280px */ + background-color: #fff; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; +} + +.pie h3 { + margin: 0 0 15px; + text-align: left; + font-size: 22px; + color: #6a4d9e; +} + +/* ====================================================== + INCOME BAR CHART AREA + ====================================================== */ +.income-bar { + flex: 1 1 55%; /* Roughly 55% of the row */ + min-width: 300px; + background-color: #fff; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; +} + +.income-bar h3 { + margin: 0 0 15px; + text-align: left; + font-size: 22px; + color: #6a4d9e; +} + +/* ====================================================== + CARDS SECTION (below pie + bar) + ====================================================== */ +.cards-section { + flex: 1 1 100%; /* Full width under the charts */ + margin-top: 10px; +} + +.cards-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + justify-content: space-between; margin-top: 10px; +} + +.card { + background-color: #fff; + padding: 1.2rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + flex: 1 1 calc(30% - 1rem); + max-width: 280px; + min-width: 220px; + transition: all 0.3s ease; + color: #333; + text-align: center; +} + +.card h4 { + margin-bottom: 0.5rem; + font-size: 1.1rem; + color: #333; +} + +.card p { + font-size: 1.4rem; + font-weight: bold; + color: #4caf50; + margin: 0; +} + +.card:hover { + background-color: #4b2c7f; + transform: translateY(-5px); +} + +.card:hover h4, +.card:hover p { + color: #fff; +} + +/* ====================================================== + TRANSACTION LINE GRAPH + ====================================================== */ +.visualization-section > div:last-child { + background-color: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; +} + +.visualization-section h3 { + margin: 0 0 15px; + text-align: left; + font-size: 22px; color: #6a4d9e; - } - - .dashboard-page .love-message { - font-size: 18px; - color: #e7a1d9; /* Pinkish love message */ - margin-top: 15px; - } - - .dashboard-page form { +} + +/* ====================================================== + FORM STYLING + ====================================================== */ +.dashboard-page form { background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); margin-top: 20px; width: 80%; - } - - .dashboard-page form div { +} + +.dashboard-page form div { margin-bottom: 15px; - } - - .dashboard-page form label { +} + +.dashboard-page form label { font-size: 16px; color: #4b2c7f; display: block; margin-bottom: 5px; - } - - .dashboard-page form input { +} + +.dashboard-page form input { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #ddd; font-size: 16px; color: #333; - } - - .dashboard-page form input:focus { +} + +.dashboard-page form input:focus { outline: none; border-color: #4b2c7f; - } - - .dashboard-page form button { +} + +.dashboard-page form button { background-color: #4b2c7f; color: #fff; padding: 10px 20px; @@ -155,13 +347,16 @@ html, body { font-size: 16px; cursor: pointer; margin-right: 10px; - } - - .dashboard-page form button:hover { +} + +.dashboard-page form button:hover { background-color: #3a2061; - } - - .dashboard-page button { +} + +/* ====================================================== + GENERIC BUTTON STYLING + ====================================================== */ +.dashboard-page button { background-color: #4b2c7f; color: #fff; padding: 10px 15px; @@ -170,44 +365,90 @@ html, body { font-size: 16px; cursor: pointer; margin: 10px 10px; - } - - .dashboard-page button:hover { +} + +.dashboard-page button:hover { background-color: #3a2061; - } - - .dashboard-page button[type="button"] { +} + +.dashboard-page button[type="button"] { background-color: #ccc; - } - - .dashboard-page button[type="button"]:hover { +} + +.dashboard-page button[type="button"]:hover { background-color: #b0b0b0; - } - .visualization-section { - padding: 10px 0px; - } - - .pie{ - width: 30%; - } - - .visualization-section h3 { - margin: 20px; - text-align: left; - font-size: 25px; - } - .visualization-section h2 { - margin: 2px; - text-align: center; - font-size: 25px; - } - - - @media (max-width: 768px) { - .dashboard-container { - width: 95%; - left: 10px; +} + +/* ====================================================== + TOGGLE SWITCH STYLING + ====================================================== */ +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 26px; +} + +.slider::before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #36A2EB; +} + +input:checked + .slider::before { + transform: translateX(24px); +} + +/* ====================================================== + RESPONSIVE BREAKPOINTS + ====================================================== */ +@media (max-width: 992px) { + /* Stack pie/bar vertically on medium screens */ + .pie-and-cards-container { + flex-direction: column; } - - } - \ No newline at end of file + + .pie, + .income-bar, + .cards-section { + flex: 1 1 100%; + min-width: auto; + } + + .card { + flex: 1 1 45%; + max-width: 45%; + } +} + +@media (max-width: 600px) { + .card { + flex: 1 1 100%; + max-width: 100%; + } +} diff --git a/Frontend/src/styles/layout.css b/Frontend/src/styles/layout.css index 7ce7132..dbbd6d6 100644 --- a/Frontend/src/styles/layout.css +++ b/Frontend/src/styles/layout.css @@ -1,83 +1,275 @@ +/* =========================== + layout.css — Dashboard Layout + =========================== */ + /* General Reset */ * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - /* Layout */ - .layout { - display: flex; + margin: 0; + padding: 0; + box-sizing: border-box; +} +/* Root Variables (aligned with budget.css) */ +:root { + --bg-page: #f7f5ff; + --bg-card: #ffffff; + --text-default: #333333; + --text-muted: #6a4d9e; + --primary: #4b2c7f; + --primary-hover: #3a2061; + --accent: rgba(255, 105, 180, 0.7); + --shadow-light: 0 4px 12px rgba(0, 0, 0, 0.08); + --radius: 12px; + --transition: all 0.3s ease; +} + +/* Ensure full page scrollability */ +html, body { + height: 100%; + min-height: 100%; + overflow-x: hidden; + scroll-behavior: smooth; + background-color: var(--bg-page); +} + +/* Layout */ +.layout { + display: flex; + flex-direction: column; /* Stack sidebar and content vertically if needed */ + min-height: 100vh; /* Allow content to expand beyond viewport */ +} + +/* Main Content */ +.main-content { + flex: 1; + margin-left: 0; + transition: margin-left 0.3s ease; + padding: 20px; + min-height: 100vh; /* Ensure main content can expand */ +} + +/* Sidebar offset */ +.main-content.sidebar-open { + margin-left: 250px; +} + +/* Sidebar */ +.sidebar { + z-index: 100; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + width: 250px; + height: 100vh; + position: fixed; + top: 0; + left: -250px; + transition: left 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); +} + +.sidebar.visible { + left: 0; +} + +.sidebar-list { + margin-top: 40px; + list-style: none; + padding: 20px; +} + +.sidebar-item { + padding: 15px 20px; + margin: 10px 0; + font-size: 18px; + font-weight: 600; + color: #fff; + border-radius: 8px; + cursor: pointer; + transition: var(--transition); +} + +.sidebar-item:hover { + background: var(--accent); + transform: translateX(5px); +} + +/* Menu Icon */ +.menu-icon { + z-index: 101; + position: fixed; + top: 20px; + left: 20px; + font-size: 28px; + color: var(--primary); + cursor: pointer; + transition: var(--transition); +} + +.menu-icon:hover { + color: var(--primary-hover); +} + +/* Animations */ +@keyframes fadeIn { + 0% { + opacity: 0; + transform: translateY(10px); } - - /* Sidebar */ - .sidebar { - z-index: 100; - background:rgba(223, 74, 162, 0.5);; ; /* Light pastel purple */ - width: 250px; - height: 100vh; - position: fixed; - top: 0; - left: -250px; - transition: left 0.3s ease; - box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); - } - - .sidebar.visible { - z-index: 100; - left: 0; + 100% { + opacity: 1; + transform: translateY(0); } - - .sidebar-list { - margin-top: 30px; - list-style: none; - padding: 20px; +} + +.dashboard { + animation: fadeIn 0.5s ease-in-out; +} + +/* Chat Button */ +.chat-button { + position: fixed; + bottom: 30px; + right: 30px; + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + border: none; + border-radius: 50%; + width: 60px; + height: 60px; + color: #fff; + font-size: 24px; + cursor: pointer; + z-index: 102; + box-shadow: var(--shadow-light); + transition: var(--transition); +} + +.chat-button:hover { + transform: scale(1.1); +} + +/* Chat Box */ +.chat-box { + position: fixed; + bottom: 100px; + right: 30px; + width: 320px; + height: 420px; + background-color: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow-light); + z-index: 101; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Chat Header */ +.chat-header { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + color: #fff; + padding: 12px 15px; + display: flex; + justify-content: space-between; + font-weight: 600; + font-size: 1.1rem; +} + +/* Chat Body */ +.chat-body { + flex: 1; + padding: 15px; + overflow-y: auto; + background-color: #fafafa; +} + +/* Close Button */ +.close-btn { + background: none; + border: none; + color: #fff; + font-size: 20px; + cursor: pointer; + transition: var(--transition); +} + +.close-btn:hover { + color: var(--accent); +} + +/* Chat Messages */ +.chat-message { + margin-bottom: 10px; + padding: 12px; + border-radius: 10px; + max-width: 85%; + word-wrap: break-word; + font-size: 1rem; + line-height: 1.5; +} + +.chat-message.user { + background-color: var(--accent); + color: #fff; + align-self: flex-end; + margin-left: auto; +} + +.chat-message.bot { + background-color: #e0e0e0; + color: var(--text-default); + align-self: flex-start; + margin-right: auto; +} + +/* Chat Input */ +.chat-input-container { + display: flex; + padding: 12px; + border-top: 1px solid #e0e0e0; + background-color: var(--bg-card); +} + +.chat-input-container input { + flex: 1; + padding: 10px 15px; + border: 1px solid #e0e0e0; + border-radius: 20px; + outline: none; + font-size: 1rem; +} + +.chat-input-container button { + background: none; + border: none; + color: var(--primary); + font-size: 22px; + margin-left: 10px; + cursor: pointer; + transition: var(--transition); +} + +.chat-input-container button:hover { + color: var(--primary-hover); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .sidebar { + width: 200px; + left: -200px; } - - .sidebar-item { - z-index: 100; - padding: 15px; - margin: 10px 0; - font-size: 18px; - color: #4b2c7f; - font-weight: 600; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s, color 0.3s; + + .main-content.sidebar-open { + margin-left: 200px; } - - .sidebar-item:hover { - z-index: 100; - background: #ce93d8; /* Soft pastel lavender */ - color: #fff; + + .chat-box { + width: 90%; + right: 5%; + bottom: 80px; + height: 380px; } - - /* Menu Icon */ + .menu-icon { - z-index: 101; - position: fixed; - top: 20px; - left: 20px; font-size: 24px; - color: #fff; - cursor: pointer; - } - - - /* Animations */ - @keyframes fadeIn { - 0% { - opacity: 0; - transform: translateY(10px); - } - 100% { - opacity: 1; - transform: translateY(0); - } - } - - .dashboard { - animation: fadeIn 0.5s ease-in-out; } - \ No newline at end of file +} \ No newline at end of file diff --git a/Frontend/src/styles/recommendation.css b/Frontend/src/styles/recommendation.css index 1a7d559..78c3c38 100644 --- a/Frontend/src/styles/recommendation.css +++ b/Frontend/src/styles/recommendation.css @@ -2,33 +2,34 @@ .recommendation-container { display: grid; grid-template-columns: 1fr; /* Single-column layout */ - gap: 16px; + gap: 8px; padding: 16px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; - max-width: 600px; /* Restrict the width */ + width: 100%; /* Take full width of parent */ word-wrap: break-word; /* Prevent long words from overflowing */ - overflow: hidden; /* Ensure no content spills out */ - } - - .recommendation-container:hover { + overflow-y: auto; /* Enable vertical scrolling */ + height: 100%; /* Take full height of parent */ +} + +.recommendation-container:hover { transform: translateY(-5px); box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); - } - - /* Header for the recommendation section */ - .recommendation-header { +} + +/* Header for the recommendation section */ +.recommendation-header { font-size: 1.8rem; color: #333; margin-bottom: 16px; font-weight: 600; - } - - /* Button styles */ - .recommendation-button { +} + +/* Button styles */ +.recommendation-button { display: inline-block; padding: 10px 20px; font-size: 1rem; @@ -39,34 +40,34 @@ border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; - } - - .recommendation-button:hover { +} + +.recommendation-button:hover { background-color: #0056b3; - } - - .recommendation-button:active { +} + +.recommendation-button:active { background-color: #004080; - } - - /* Loading and error text */ - .loading-text, - .error-text { +} + +/* Loading and error text */ +.loading-text, +.error-text { font-size: 1rem; text-align: center; margin: 10px 0; - } - - .loading-text { +} + +.loading-text { color: #666; - } - - .error-text { +} + +.error-text { color: #e74c3c; - } - - /* Recommendation data container */ - .recommendation-data { +} + +/* Recommendation data container */ +.recommendation-data { padding: 10px; background-color: #fff; border: 1px solid #ddd; @@ -78,19 +79,64 @@ overflow-x: auto; /* Allow horizontal scrolling for long JSON strings */ max-width: 100%; /* Ensure the data doesn't exceed the container width */ box-sizing: border-box; /* Include padding and border in width calculation */ - } - - /* Overall page container */ - .page-container { +} + +/* Overall page container */ +.page-container { display: flex; flex-direction: column; gap: 20px; padding: 20px; - } + height: 100%; + overflow-y: auto; /* Enable vertical scrolling */ +} + +/* Recommendation list container */ +.recommendation-list { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; /* Enable vertical scrolling */ + max-height: calc(100vh - 250px); /* Leave space for header and pagination */ + padding-right: 8px; /* Add space for scrollbar */ +} + +/* Recommendation item */ +.recommendation-item { + background-color: #fff; + padding: 16px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease; +} + +.recommendation-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +/* Scrollbar styling */ +.recommendation-list::-webkit-scrollbar { + width: 8px; +} + +.recommendation-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.recommendation-list::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.recommendation-list::-webkit-scrollbar-thumb:hover { + background: #666; +} - p.text{ +p.text{ color: #2e2c2c; font-family: monospace; font-size: 0.9rem; - } +} \ No newline at end of file diff --git a/Frontend/src/types/transaction.ts b/Frontend/src/types/transaction.ts index f7bb485..48b7a4d 100644 --- a/Frontend/src/types/transaction.ts +++ b/Frontend/src/types/transaction.ts @@ -1,19 +1,28 @@ import { Category } from "./category"; - - export interface Transaction { - _id: string; - type: 'income' | 'expense'; - category: Category; - amount: number; - date: string; - description?: string; - userId: string; - } - - export interface TransactionFilters { - date?: string; - category?: string; - } - - \ No newline at end of file +export interface Transaction { + _id: string; + type: 'income' | 'expense'; + category: Category; // full object when returned + amount: number; + date: string; + description?: string; + userId: string; +} + +// Filters for list view +export interface TransactionFilters { + date?: string; + category?: string; + month?: string; + year?: string; +} + +// DTO for adding a transaction +export type CreateTransactionDTO = { + type: 'income' | 'expense'; + category: string; + amount: number; + date: string; + description?: string; +}; \ No newline at end of file diff --git a/Frontend/test-results/.last-run.json b/Frontend/test-results/.last-run.json index df78c0e..cbcc1fb 100644 --- a/Frontend/test-results/.last-run.json +++ b/Frontend/test-results/.last-run.json @@ -1,6 +1,4 @@ { - "status": "failed", - "failedTests": [ - "ba0db3d088059a45db0d-20bc3e21613bb8009c6a" - ] + "status": "passed", + "failedTests": [] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6439532..4bfd6c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "testing_project", + "name": "Testing_Project_Improved", "lockfileVersion": 3, "requires": true, "packages": {}