diff --git a/client/package-lock.json b/client/package-lock.json index f692b13..5919ebd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -20,6 +20,7 @@ "react-modal": "^3.16.1", "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", + "recharts": "^2.10.3", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -3990,6 +3991,60 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", + "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -6040,6 +6095,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6704,6 +6767,116 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6743,6 +6916,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6991,6 +7169,14 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8169,6 +8355,14 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -9374,6 +9568,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -14998,6 +15200,35 @@ } } }, + "node_modules/react-smooth": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", + "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", + "dependencies": { + "fast-equals": "^5.0.0", + "react-transition-group": "2.9.0" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15030,6 +15261,42 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.3.tgz", + "integrity": "sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-smooth": "^2.0.5", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prop-types": "^15.6.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -17139,6 +17406,27 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.7.0", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", + "integrity": "sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -20783,6 +21071,60 @@ "@types/node": "*" } }, + "@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "requires": { + "@types/d3-color": "*" + } + }, + "@types/d3-path": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", + "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==" + }, + "@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "requires": { + "@types/d3-path": "*" + } + }, + "@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -22351,6 +22693,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -22822,6 +23169,83 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -22850,6 +23274,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -23044,6 +23473,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -23921,6 +24358,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==" + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -24780,6 +25222,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -28653,6 +29100,26 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-smooth": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", + "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", + "requires": { + "fast-equals": "^5.0.0", + "react-transition-group": "2.9.0" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -28679,6 +29146,36 @@ "picomatch": "^2.2.1" } }, + "recharts": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.3.tgz", + "integrity": "sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==", + "requires": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.19", + "react-is": "^16.10.2", + "react-smooth": "^2.0.5", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "requires": { + "decimal.js-light": "^2.4.1" + } + }, "recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -30227,6 +30724,27 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "victory-vendor": { + "version": "36.7.0", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", + "integrity": "sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==", + "requires": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/client/package.json b/client/package.json index 4da6a0f..f19004e 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "react-modal": "^3.16.1", "react-router-dom": "^6.16.0", "react-scripts": "5.0.1", + "recharts": "^2.10.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/client/src/App.js b/client/src/App.js index 733b6dc..5256ad9 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -59,6 +59,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/client/src/components/admin-dashboard.jsx b/client/src/components/admin-dashboard.jsx new file mode 100644 index 0000000..6960db6 --- /dev/null +++ b/client/src/components/admin-dashboard.jsx @@ -0,0 +1,306 @@ +import React, {useState, useEffect} from 'react'; +import Orders from './orderManager.jsx' +import DashInfoCard from './dashboardInfoCard'; +import { FaTag } from "react-icons/fa6"; +import { BsArrowRepeat } from "react-icons/bs"; +import { FcOk } from "react-icons/fc"; +import { FaStar } from "react-icons/fa"; +import { MdTableRestaurant } from "react-icons/md"; + +const Dashboard = () => { + + // const [sales, setSales] = useState(172845); + //for the incomplete/open orders + const [pending, setPending] = useState(0); + //for the completed / processed orders; + const [completed, setCompleted] = useState(0); + const [rating, setRatings] = useState(4); + // const [tablesAvailable, setTablesAvailable] = useState(8); + + //mock restaurant data for getting user and order history + const [restaurantData, setRestaurantData] = useState({ + restaurantName: 'Your Restaurant', + restaurantMenu: { + totalItemCount: 2, + menuList: [ + { + menuId: '1', + image: 'menu-item-1.jpg', + filter: ['filter1', 'filter2'], + name: 'Menu Item 1', + price: 10, + description: 'Description for Menu Item 1', + diet: ['vegan'], + customizable: true, + custom: [ + { + name: 'Custom Option 1', + multipleSelection: true, + option: [ + { customName: 'Option A', price: 2 }, + { customName: 'Option B', price: 3 }, + ], + }, + ], + }, + { + menuId: '2', + image: 'menu-item-2.jpg', + filter: ['filter1', 'filter3'], + name: 'Menu Item 2', + price: 15, + description: 'Description for Menu Item 2', + diet: ['vegetarian'], + customizable: false, + custom: [], + }, + { + menuId: '3', + image: 'menu-item-3.jpg', + filter: ['filter1', 'filter3'], + name: 'Menu Item 3', + price: 15, + description: 'Description for Menu Item 3', + diet: ['vegan'], + customizable: false, + custom: [], + }, + ], + }, + table: { + totalTableCount: 5, + tableList: [ + { + seatCapacity: 4, + isOccupied: false, + order: [ { + menuItemId: 1, + quantity: 2, + custom: [], + price: 20, + status: 'completed', + + }, + + ], + }, + { + seatCapacity: 4, + isOccupied: true, + order: [ { + menuItemId: 1, + quantity: 2, + custom: [], + price: 20, + status: 'pending', + + }, + + ], + }, + + ], + }, + history: [ + { + userId: 'user1', + userHistory: [ + { + menuItemId: '1', + quantity: 2, + custom: [], + price: 20, + createdAt: new Date('2023-01-02T18:45:00'), + }, + { + menuItemId: '2', + quantity: 1, + custom: ['extra cheese'], + price: 15, + createdAt: new Date('2023-01-02T18:45:00'), + }, + ], + }, + { + userId: 'user2', + userHistory: [ + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-12-12T18:45:00'), + }, + { + menuItemId: '3', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-12-12T18:45:00'), + }, + + ], + }, + { + userId: 'user3', + userHistory: [ + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T12:45:00'), + }, + { + menuItemId: '3', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T12:45:00'), + }, + + ], + }, + { + userId: 'user4', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + + ], + }, + { + userId: 'user5', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + // add user + ], + }, + //add more history + + ], + }); + + //get the order status of tables checking if processed or pending + useEffect(() => { + // Calculate total processed orders based on restaurantData + const calculateOrderStats = () => { + if (!restaurantData || !restaurantData.table || !restaurantData.table.tableList) { + return; + } + + let totalCompletedOrder = 0; + let totalPendingOrder = 0 + restaurantData.table.tableList.forEach(table => { + if (table.order && table.order.length > 0) { + totalPendingOrder += table.order.filter(order => order.status === 'pending').length; + totalCompletedOrder += table.order.filter(order => order.status === 'completed').length; + } + }); + + setCompleted(totalCompletedOrder); + setPending(totalPendingOrder); + + }; + + + calculateOrderStats(); + }, [restaurantData]); + + //total sales from restaurnat data history + const calculateTotalSales = () => { + if (!restaurantData || !restaurantData.history) { + return 0; + } + + return restaurantData.history.reduce((totalSales, history) => { + if (history.userHistory) { + history.userHistory.forEach(order => { + totalSales += order.price || 0; + }); + } + return totalSales; + }, 0); + + } + + + + const totalSales = calculateTotalSales().toLocaleString(); + + + + return( +
+
+ +
+ } stats={totalSales}/> + } stats={completed}/> + } stats={pending}/> + } stats={rating}/> + {/* } stats={tablesAvailable}/> */} + + + +
+
+ +
+ ) +} + +export default Dashboard; diff --git a/client/src/components/analyticsCard.jsx b/client/src/components/analyticsCard.jsx new file mode 100644 index 0000000..fd47776 --- /dev/null +++ b/client/src/components/analyticsCard.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const AnalyticsCard = ({description, icon, stat}) => { + + return ( +
+

{description}

+ +
+
{icon}
+ +

{stat}

+
+ + +
+ + ) +} + +export default AnalyticsCard; \ No newline at end of file diff --git a/client/src/components/analyticsManager.jsx b/client/src/components/analyticsManager.jsx new file mode 100644 index 0000000..f101cfb --- /dev/null +++ b/client/src/components/analyticsManager.jsx @@ -0,0 +1,673 @@ +import React, { useState } from 'react'; +import { IoMdRestaurant } from "react-icons/io"; +//import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; +import { PieChart, Pie, Tooltip, Legend, Cell } from 'recharts'; +import { BarChart, Bar, XAxis, YAxis} from 'recharts'; +import AnalyticsCard from './analyticsCard' +import { MdFastfood } from "react-icons/md"; +import { FiDollarSign } from "react-icons/fi"; +import { MdPeopleAlt } from "react-icons/md"; +import { FaMoneyBillWave } from "react-icons/fa"; +import { TbArrowZigZag } from "react-icons/tb"; +const Analytics = () => { + + //colors for the pie chart + const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#AF19FF', '#FF4D4F', '#36CFC9']; + + //mock data for fake restaurant containing menu , tables, and history + const [restaurantData, setRestaurantData] = useState({ + restaurantName: 'Your Restaurant', + restaurantMenu: { + totalItemCount: 2, + menuList: [ + { + menuId: '1', + image: 'menu-item-1.jpg', + filter: ['filter1', 'filter2'], + name: 'Menu Item 1', + price: 10, + description: 'Description for Menu Item 1', + diet: ['vegan'], + customizable: true, + custom: [ + { + name: 'Custom Option 1', + multipleSelection: true, + option: [ + { customName: 'Option A', price: 2 }, + { customName: 'Option B', price: 3 }, + ], + }, + ], + }, + { + menuId: '2', + image: 'menu-item-2.jpg', + filter: ['filter1', 'filter3'], + name: 'Menu Item 2', + price: 15, + description: 'Description for Menu Item 2', + diet: ['vegetarian'], + customizable: false, + custom: [], + }, + { + menuId: '3', + image: 'menu-item-3.jpg', + filter: ['filter1', 'filter3'], + name: 'Menu Item 3', + price: 15, + description: 'Description for Menu Item 3', + diet: ['vegan'], + customizable: false, + custom: [], + }, + ], + }, + table: { + totalTableCount: 5, + tableList: [ + { + seatCapacity: 4, + isOccupied: false, + order: [], + }, + + ], + }, + history: [ + { + userId: 'user1', + userHistory: [ + { + menuItemId: '1', + quantity: 2, + custom: [], + price: 20, + createdAt: new Date('2023-01-02T18:45:00'), + }, + { + menuItemId: '2', + quantity: 1, + custom: ['extra cheese'], + price: 15, + createdAt: new Date('2023-01-02T18:45:00'), + }, + ], + }, + { + userId: 'user2', + userHistory: [ + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-12-12T18:45:00'), + }, + { + menuItemId: '3', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-12-12T18:45:00'), + }, + // Add more order histories as needed + ], + }, + { + userId: 'user3', + userHistory: [ + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T12:45:00'), + }, + { + menuItemId: '3', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T12:45:00'), + }, + // Add more order histories as needed + ], + }, + { + userId: 'user4', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + // Add more order histories as needed + ], + }, + { + userId: 'user5', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T14:45:00'), + }, + // Add more order histories as needed + ], + }, + { + userId: 'use6', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T10:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T10:45:00'), + }, + { + menuItemId: '3', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T10:45:00'), + }, + + // Add more order histories as needed + ], + }, + + { + userId: 'user7', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T13:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T13:45:00'), + }, + + + // Add more order histories as needed + ], + }, + + { + userId: 'user9', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T13:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T13:45:00'), + }, + + + // Add more order histories as needed + ], + }, + { + userId: 'user10', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-02T13:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-02T13:45:00'), + }, + + + // Add more order histories as needed + ], + }, + { + userId: 'user11', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-01-03T20:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-01-03T20:45:00'), + }, + + + // Add more order histories as needed + ], + }, + { + userId: 'user12', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-05-03T11:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-05-03T11:45:00'), + }, + + + // Add more order histories as needed + ], + }, + { + userId: 'user13', + userHistory: [ + { + menuItemId: '2', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2021-05-03T09:45:00'), + }, + { + menuItemId: '1', + quantity: 3, + custom: ['extra sauce'], + price: 30, + createdAt: new Date('2023-05-03T09:45:00'), + }, + + + // Add more order histories as needed + ], + }, + // Add more user data as needed + ], + }); + + + //if restaurant data is not there + if (!restaurantData) { + + return

Loading...

; + } + + //total orders across all users in restaurant history + const calculateTotalOrders = () => { + if (!restaurantData || !restaurantData.history) { + return 0; + } + + return restaurantData.history.reduce( + (total, history) => total + (history.userHistory ? history.userHistory.length : 0), + 0 + ); + }; + + // get total revenue of restaurant based on user order history + const calculateTotalSales = () => { + if (!restaurantData || !restaurantData.history) { + return 0; + } + + return restaurantData.history.reduce((totalSales, history) => { + if (history.userHistory) { + history.userHistory.forEach(order => { + totalSales += order.price || 0; + }); + } + return totalSales; + }, 0); + + } + + + + //get the count of each menu item to display on pie chart to visualize most popular items + const calculateMenuItemCounts = () => { + if (!restaurantData || !restaurantData.history) { + return []; + } + + //keep count in dictionary here of all items + const itemCounts = {}; + + //go through restaurant history and each user's history + restaurantData.history.forEach((history) => { + //check if user history exists + if (history.userHistory) { + //go through each order in history + history.userHistory.forEach((order) => { + // get the item id + const menuItemId = order.menuItemId; + // find the item in list that matches id + const menuItem = restaurantData.restaurantMenu.menuList.find((item) => item.menuId === menuItemId); + + //if it exists, update count of item to keep track of total num orders of each item + if (menuItem) { + const menuItemName = menuItem.name; + itemCounts[menuItemId] = { + count: (itemCounts[menuItemId]?.count || 0) + 1, + name: menuItemName, + }; + } + }); + } + }); + + // Turn the itemCounts object to an array, this is for the pie chart data visualization + return Object.keys(itemCounts).map((menuItemId) => ({ + menuItemId, + name: itemCounts[menuItemId].name, + count: itemCounts[menuItemId].count, + })); + }; + + + //get the most popular times based on when users come to place order + const calculatePopularTimes = () => { + if (!restaurantData || !restaurantData.history) { + return []; + } + + //get count of users at a time + const userCounts = {}; + + // go through history + restaurantData.history.forEach((history) => { + if (history.userHistory) { + //go through user history + history.userHistory.forEach((order) => { + //check when order was created + if (order.createdAt) { + //get the hour of the order time + const hour = new Date(order.createdAt).getHours(); + // + const timeKey = `${hour}:00`; + + // if there is no existing user at that time, create set + if (!userCounts[timeKey]) { + userCounts[timeKey] = new Set(); + } + + //update the list of users at that time + userCounts[timeKey].add(history.userId); + } + }); + } + }); + + // Convert the userCounts object to an array of objects, used for the bar chart + return Object.keys(userCounts).map((timeKey) => ({ + hour: timeKey, + count: userCounts[timeKey].size, + })); + + }; + + //Get total unique users + const calculateTotalCustomers = () => { + if (!restaurantData || !restaurantData.history) { + return 0; + } + + //set for keeping unique users id + const uniqueUserIds = new Set(); + + //go through history + restaurantData.history.forEach((history) => { + // check if user exists, then add to set + if (history.userId) { + uniqueUserIds.add(history.userId); + } + }); + + //return the size to show how many users there are + return uniqueUserIds.size; + }; + + + //get the min and max range of menu item price + const calculatePriceRange = (menuList) => { + if (!menuList || menuList.length === 0) { + return null; + } + + //map each item in the menuList to a price + const prices = menuList.map(item => item.price); + + // Find the minimum and maximum prices + const minPrice = Math.min(...prices); + const maxPrice = Math.max(...prices); + + + return { minPrice, maxPrice }; + }; + + //get the price range based on restaurants menu list + const priceRange = calculatePriceRange(restaurantData.restaurantMenu.menuList); + + + //for popular items + const menuItemCounts = calculateMenuItemCounts(); + + //count the orders + const totalOrders = calculateTotalOrders(); + + //total revenue from items only + const totalSales = calculateTotalSales().toLocaleString(); + + //count users from history + const totalCustomers = calculateTotalCustomers(); + + //get avg spending of custoemr total + const avgCustomerSpending = totalCustomers > 0 ? (totalSales / totalCustomers).toFixed(2) : 0; + + //get times that people come in on based on order time + const popularTimesData = calculatePopularTimes(); + const sortedPopularTimesData = popularTimesData.sort((a, b) => b.count - a.count); + + return ( +
+ +
+
+ {/* Basic Restaurant Card to get the name, table count, menu count, and price range */} +
+ +
+ +

{restaurantData.restaurantName}

+ +
+ +
+
+ {restaurantData.table.totalTableCount} +
+ tables + +
+
+
+ {restaurantData.restaurantMenu.totalItemCount} +
+ items on menu + +
+
+
+ {priceRange && `$${priceRange.minPrice}-${priceRange.maxPrice}`} +
+ menu price range + +
+
+ + {/* Stat card for total orders and total revenue */} +
+ } stat={totalOrders}/> + } stat={totalSales}/> + + +
+ + {/* pie chart to show the items and visualize popularity of each item */} +
+

Popular Menu Items

+ + `${(percent * 100).toFixed(0)}%`} + > + {menuItemCounts.map((entry, index) => ( + + ))} + + ( +
+

{`Menu Item: ${payload[0]?.payload.name}`}

+

{`Total Orders: ${payload[0]?.payload.count}`}

+
+ )} + /> + + + +
+
+
+ +
+ {/* Bar Graph to show the times that are most popular based on number of users that have come in at that time in restaurant history */} +
+

Most to Least Popular Times

+ + { + const originalHour = parseInt(value, 10); + const formattedHour = originalHour % 12 || 12; // Convert to 12-hour format + const ampm = originalHour >= 12 ? 'PM' : 'AM'; + return `${formattedHour} ${ampm}`; + }} + /> + + + +
+ + {/* Stat cards for total users and avg spending of users */} +
+ } stat={totalCustomers}/> + } stat={avgCustomerSpending}/> + +
+
+ +
+ + +
+ ); + +}; + +export default Analytics; diff --git a/client/src/pages/admin-analytics.js b/client/src/pages/admin-analytics.js index 79fb6a9..922aede 100644 --- a/client/src/pages/admin-analytics.js +++ b/client/src/pages/admin-analytics.js @@ -1,6 +1,6 @@ import React from 'react' import AdminNavbar from '../components/adminNavbar' - +import Analytics from '../components/analyticsManager' const Admin_analytics = (props) => { const { WebSocketService, setPage } = props; @@ -11,7 +11,7 @@ const Admin_analytics = (props) => {
Analytics
-
{/* Component */}
+
diff --git a/client/src/pages/home.js b/client/src/pages/home.js index ace4594..3777ac6 100644 --- a/client/src/pages/home.js +++ b/client/src/pages/home.js @@ -10,6 +10,7 @@ const Home = () => { Login Menu Table + Analytics Admin dashboard diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 574cfcc..576ca2d 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -6,6 +6,11 @@ module.exports = { fontFamily: { montserrat: ['Montserrat', 'sans'],//Define Montserrat font }, + fontSize: { + '4xl': '2.5rem', + '5xl': '3rem', + + },