diff --git a/.gitignore b/.gitignore index 0c30ee3..7b8fc7d 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,4 @@ dist # Config Files config.json +adminSettings.json diff --git a/client/package-lock.json b/client/package-lock.json index 2f5d067..15c4ceb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -25,17 +25,19 @@ "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "axios": "^0.27.2", + "chart.js": "^4.4.1", + "chat.js": "^1.0.2", "downloadjs": "^1.4.7", "luxon": "^2.4.0", "material-react-toastify": "^1.0.1", "number-to-words": "^1.2.4", "react": "^18.1.0", + "react-chartjs-2": "^5.2.0", "react-cookie": "^4.1.1", "react-dom": "^18.1.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-window": "^1.8.7", - "recharts": "^2.1.13", "socket.io-client": "^4.5.1", "styled-components": "^5.3.5", "typescript": "^4.8.4", @@ -3176,6 +3178,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -6474,6 +6481,22 @@ "node": ">=6" } }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, + "node_modules/chat.js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chat.js/-/chat.js-1.0.2.tgz", + "integrity": "sha512-0Q0FnNyFc+xiZoe2dexBbHBDf2F9CSt2bpBgjZ8gv4yghgTrv+G/EB9nsUDdt+lA0F/IOb+PlHPoky1SHDMtqQ==" + }, "node_modules/check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -6534,11 +6557,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" }, - "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, "node_modules/clean-css": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", @@ -7111,11 +7129,6 @@ "node": ">=0.10.0" } }, - "node_modules/css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" - }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -7295,100 +7308,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, - "node_modules/d3-array": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", - "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", - "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-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.0.1", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", - "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", - "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.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", - "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", - "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/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7428,11 +7347,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" }, - "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/decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -8846,11 +8760,6 @@ "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": "2.0.4", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz", - "integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==" - }, "node_modules/fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", @@ -10006,14 +9915,6 @@ "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.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -14951,6 +14852,15 @@ "node": ">=14" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-cookie": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", @@ -15103,11 +15013,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15116,18 +15021,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-resize-detector": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz", - "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -15232,43 +15125,6 @@ "node": ">=10" } }, - "node_modules/react-smooth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.1.tgz", - "integrity": "sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA==", - "dependencies": { - "fast-equals": "^2.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-smooth/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/react-smooth/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/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -15324,45 +15180,6 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.13.tgz", - "integrity": "sha512-9VWu2nzExmfiMFDHKqRFhYlJVmjzQGVKH5rBetXR4EuyEXuu3Y6cVxQuNEdusHhbm4SoPPrVDCwlBdREL3sQPA==", - "dependencies": { - "classnames": "^2.2.5", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", - "react-is": "^16.10.2", - "react-resize-detector": "^7.1.2", - "react-smooth": "^2.0.1", - "recharts-scale": "^0.4.4", - "reduce-css-calc": "^2.1.8" - }, - "engines": { - "node": ">=12" - }, - "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.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -15397,20 +15214,6 @@ "node": ">=8" } }, - "node_modules/reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "dependencies": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - } - }, - "node_modules/reduce-css-calc/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -20315,6 +20118,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -22603,6 +22411,19 @@ "resolved": "https://registry.npmjs.org/charcodes/-/charcodes-0.2.0.tgz", "integrity": "sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==" }, + "chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, + "chat.js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chat.js/-/chat.js-1.0.2.tgz", + "integrity": "sha512-0Q0FnNyFc+xiZoe2dexBbHBDf2F9CSt2bpBgjZ8gv4yghgTrv+G/EB9nsUDdt+lA0F/IOb+PlHPoky1SHDMtqQ==" + }, "check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -22648,11 +22469,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, "clean-css": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", @@ -23065,11 +22881,6 @@ } } }, - "css-unit-converter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" - }, "css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -23196,73 +23007,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, - "d3-array": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", - "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", - "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-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.0.1", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", - "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==" - }, - "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.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", - "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", - "requires": { - "d3-path": "1 - 3" - } - }, - "d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", - "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" - } - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -23291,11 +23035,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" }, - "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==" - }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -24331,11 +24070,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "fast-equals": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz", - "integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==" - }, "fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", @@ -25170,11 +24904,6 @@ "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.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -28598,6 +28327,12 @@ "whatwg-fetch": "^3.6.2" } }, + "react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "requires": {} + }, "react-cookie": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", @@ -28713,24 +28448,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, - "react-resize-detector": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz", - "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==", - "requires": { - "lodash": "^4.17.21" - } - }, "react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -28810,36 +28532,6 @@ } } }, - "react-smooth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.1.tgz", - "integrity": "sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA==", - "requires": { - "fast-equals": "^2.0.0", - "react-transition-group": "2.9.0" - }, - "dependencies": { - "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" - } - }, - "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" - } - } - } - }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -28878,39 +28570,6 @@ "picomatch": "^2.2.1" } }, - "recharts": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.13.tgz", - "integrity": "sha512-9VWu2nzExmfiMFDHKqRFhYlJVmjzQGVKH5rBetXR4EuyEXuu3Y6cVxQuNEdusHhbm4SoPPrVDCwlBdREL3sQPA==", - "requires": { - "classnames": "^2.2.5", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", - "react-is": "^16.10.2", - "react-resize-detector": "^7.1.2", - "react-smooth": "^2.0.1", - "recharts-scale": "^0.4.4", - "reduce-css-calc": "^2.1.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.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -28938,22 +28597,6 @@ "strip-indent": "^3.0.0" } }, - "reduce-css-calc": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", - "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", - "requires": { - "css-unit-converter": "^1.1.1", - "postcss-value-parser": "^3.3.0" - }, - "dependencies": { - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - } - } - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/client/package.json b/client/package.json index fc7b4e4..8933246 100644 --- a/client/package.json +++ b/client/package.json @@ -20,17 +20,19 @@ "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "axios": "^0.27.2", + "chart.js": "^4.4.1", + "chat.js": "^1.0.2", "downloadjs": "^1.4.7", "luxon": "^2.4.0", "material-react-toastify": "^1.0.1", "number-to-words": "^1.2.4", "react": "^18.1.0", + "react-chartjs-2": "^5.2.0", "react-cookie": "^4.1.1", "react-dom": "^18.1.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-window": "^1.8.7", - "recharts": "^2.1.13", "socket.io-client": "^4.5.1", "styled-components": "^5.3.5", "typescript": "^4.8.4", diff --git a/client/src/components/home/shared/AskQuestion.tsx b/client/src/components/home/shared/AskQuestion.tsx index b63e5ad..f893c33 100644 --- a/client/src/components/home/shared/AskQuestion.tsx +++ b/client/src/components/home/shared/AskQuestion.tsx @@ -10,7 +10,6 @@ import BaseCard from '../../common/cards/BaseCard'; import HomeService from '../../../services/HomeService'; import {UserDataContext} from '../../../contexts/UserDataContext'; import {QueueDataContext} from '../../../contexts/QueueDataContext'; -import {StudentDataContext} from '../../../contexts/StudentDataContext'; function createData(assignment_id, name) { return {assignment_id, name}; @@ -34,8 +33,6 @@ export default function AskQuestion() { const [askDisabled, setAskDisabled] = useState(false); - const {studentData, setStudentData} = useContext(StudentDataContext); - const locations = useMemo(() => { if (queueData != null) { const day = date.getDay(); diff --git a/client/src/components/home/shared/QueueStats.tsx b/client/src/components/home/shared/QueueStats.tsx index 3e636f0..74af2f5 100644 --- a/client/src/components/home/shared/QueueStats.tsx +++ b/client/src/components/home/shared/QueueStats.tsx @@ -1,4 +1,4 @@ -import React, {useContext, useEffect} from 'react'; +import React, {useContext} from 'react'; import { CardContent, Divider, Stack, Typography, useTheme, } from '@mui/material'; diff --git a/client/src/components/home/student/UpdateQuestionOverlay.tsx b/client/src/components/home/student/UpdateQuestionOverlay.tsx index 4939ba5..eb1181b 100644 --- a/client/src/components/home/student/UpdateQuestionOverlay.tsx +++ b/client/src/components/home/student/UpdateQuestionOverlay.tsx @@ -1,6 +1,6 @@ import React, {useContext, useState} from 'react'; import { - Button, Dialog, DialogContent, FormControl, Input, Link, Stack, Typography, + Button, Dialog, DialogContent, FormControl, Input, Link, Typography, } from '@mui/material'; import HomeService from '../../../services/HomeService'; diff --git a/client/src/components/home/ta/dialogs/FilterOptions.tsx b/client/src/components/home/ta/dialogs/FilterOptions.tsx index 0fc3200..4f234d3 100644 --- a/client/src/components/home/ta/dialogs/FilterOptions.tsx +++ b/client/src/components/home/ta/dialogs/FilterOptions.tsx @@ -1,10 +1,9 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useContext, useMemo} from 'react'; import {List, ListSubheader, ListItem, ListItemButton, ListItemIcon, ListItemText, Checkbox, } from '@mui/material'; -import SettingsService from '../../../../services/SettingsService'; import {QueueDataContext} from '../../../../contexts/QueueDataContext'; function createData(assignment_id, name) { diff --git a/client/src/components/metrics/AdminMetrics.tsx b/client/src/components/metrics/AdminMetrics.tsx new file mode 100644 index 0000000..9e9ea73 --- /dev/null +++ b/client/src/components/metrics/AdminMetrics.tsx @@ -0,0 +1,182 @@ +import React, {useEffect, useState} from 'react'; +import MetricsService from '../../services/MetricsService'; +import { + Card, Divider, Typography, Stack, Table, TableBody, TableCell, + TableContainer, TableHead, TablePagination, TableRow, +} from '@mui/material'; + +export default function AdminMetrics() { + const [rankedStudents, setRankedStudents] = useState([]); + const [rankedTAs, setRankedTAs] = useState([]); + + // students pagination + const [studentPage, setStudentPage] = useState(0); + const [rowsPerStudentPage, setRowsPerStudentPage] = useState(10); + const handleChangeStudentPage = (event, newPage) => { + setStudentPage(newPage); + }; + const handleChangeRowsPerStudentPage = (event) => { + setRowsPerStudentPage(+event.target.value); + setStudentPage(0); + }; + + // tas pagination + const [taPage, setTAPage] = useState(0); + const [rowsPerTAPage, setRowsPerTAPage] = useState(10); + const handleChangeTAPage = (event, newPage) => { + setTAPage(newPage); + }; + const handleChangeRowsPerTAPage = (event) => { + setRowsPerTAPage(+event.target.value); + setTAPage(0); + }; + + useEffect(() => { + MetricsService.getRankedStudents().then((res) => { + setRankedStudents(res.data.rankedStudents.map((student) => { + return { + ...student, + average: Math.round(student.timeHelped / student.count * 10) / 10, + }; + })); + }); + + MetricsService.getRankedTAs().then((res) => { + setRankedTAs(res.data.rankedTAs.map((ta) => { + return { + ...ta, + average: Math.round(ta.timeHelping / ta.count * 10) / 10, + }; + })); + }); + }, []); + + const studentCols = [ + {id: 'student_andrew', label: 'Andrew ID', width: 25}, + {id: 'student_name', label: 'Name', width: 25}, + {id: 'count', label: 'Num Questions', width: 100}, + {id: 'badCount', label: 'Num Ask to Fix', width: 100}, + {id: 'timeHelped', label: 'Total Helping Time (min)', width: 100}, + {id: 'average', label: 'Average Helping Time (min)', width: 100}, + ]; + + const taCols = [ + {id: 'ta_andrew', label: 'Andrew ID', width: 25}, + {id: 'ta_name', label: 'Name', width: 25}, + {id: 'count', label: 'Num Questions Answered', width: 100}, + {id: 'timeHelping', label: 'Total Time Helping (min)', width: 100}, + {id: 'average', label: 'Average Time Helping (min)', width: 100}, + ]; + + return ( +
+ + Ranked Students and Ranked TAs + + + + } + spacing={2} + sx={{m: 2}} + > + + + + + + {studentCols.map((column) => ( + + {column.label} + + ))} + + + + {rankedStudents + .slice(studentPage * rowsPerStudentPage, studentPage * rowsPerStudentPage + rowsPerStudentPage) + .map((row) => { + return ( + + {studentCols.map((column) => { + const value = row[column.id]; + return ( + + {value} + + ); + })} + + ); + }) + } + +
+
+ +
+ + + + + + {taCols.map((column) => ( + + {column.label} + + ))} + + + + {rankedTAs + .slice(taPage * rowsPerTAPage, taPage * rowsPerTAPage + rowsPerTAPage) + .map((row) => { + return ( + + {taCols.map((column) => { + const value = row[column.id]; + return ( + + {value} + + ); + })} + + ); + }) + } + +
+
+ +
+
+
+
+ ); +} diff --git a/client/src/components/metrics/Graph.tsx b/client/src/components/metrics/Graph.tsx index 6366d58..383b749 100644 --- a/client/src/components/metrics/Graph.tsx +++ b/client/src/components/metrics/Graph.tsx @@ -4,7 +4,10 @@ import { } from '@mui/material'; import {DateTime} from 'luxon'; -import {ResponsiveContainer, LineChart, BarChart, Bar, Line, Label, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts'; + +import {Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip as Tooltip2, Legend} from 'chart.js'; +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip2, Legend); +import {Line, Bar} from 'react-chartjs-2'; import MetricsService from '../../services/MetricsService'; @@ -20,7 +23,14 @@ export default function Graph() { }); MetricsService.getNumStudentsPerDay().then((res) => { - setNumStudentsPerDay(res.data.numStudentsPerDay); + const dataBack = res.data.numStudentsPerDay; + // sort by day of week + dataBack.sort((a, b) => { + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + return days.indexOf(a.day) - days.indexOf(b.day); + }); + + setNumStudentsPerDay(dataBack); }); MetricsService.getNumStudentsOverall().then((res) => { @@ -35,86 +45,208 @@ export default function Graph() { return (
- Number of Students per Day (in the last week) + Number of Questions per Day (in the last week) - - - - - - - - - - - + +
+ dateFormatter(day.day)), + datasets: [ + { + label: 'Number of Students', + data: numStudentsPerDayLastWeek.map((day) => day.students), + fill: false, + backgroundColor: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + borderWidth: 3, + tension: 0.3, + }, + ], + }} + /> +
- Number of Students per Day (overall) + Number of Questions per Day (semester) - - - - - - - - - - - + +
+ dateFormatter(day.day)), + datasets: [ + { + label: 'Number of Students', + data: numStudentsOverall.map((day) => day.students), + fill: false, + backgroundColor: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + borderWidth: 3, + tension: 0.3, + }, + ], + }} + /> +
- Number of Students per Day of the Week + Number of Questions per Day of the Week - - - - - - +
+ day.day), + datasets: [ + { + label: 'Number of Students', + data: numStudentsPerDay.map((day) => day.students), + backgroundColor: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + borderWidth: 3, + }, + ], + }} + /> +
); } diff --git a/client/src/components/metrics/MetricsMain.tsx b/client/src/components/metrics/MetricsMain.tsx index 0aa3898..247238d 100644 --- a/client/src/components/metrics/MetricsMain.tsx +++ b/client/src/components/metrics/MetricsMain.tsx @@ -1,26 +1,32 @@ -import React from 'react'; +import React, {useContext} from 'react'; import { Typography, } from '@mui/material'; -import DateTimeSelector from './DateTimeSelector'; import PersonalStats from './PersonalStats'; import OverallStats from './OverallStats'; import CumulativeStats from './CumulativeStats'; import Graph from './Graph'; +import {UserDataContext} from '../../contexts/UserDataContext'; +import AdminMetrics from './AdminMetrics'; export default function MetricsMain(props) { + const {userData} = useContext(UserDataContext); + return (
Metrics - {/* */} + + { + userData.isAdmin && + }
); } diff --git a/client/src/components/metrics/PersonalStats.tsx b/client/src/components/metrics/PersonalStats.tsx index f2f205d..746d29f 100644 --- a/client/src/components/metrics/PersonalStats.tsx +++ b/client/src/components/metrics/PersonalStats.tsx @@ -90,7 +90,7 @@ export default function PersonalStats() { {Number(averageHelpTime).toFixed(2)} - + @@ -107,9 +107,9 @@ export default function PersonalStats() { {helpedStudents .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => { + .map((row, i) => { return ( - + {columns.map((column) => { const value = row[column.id]; return ( diff --git a/client/src/components/navbar/ChangeNameBtn.tsx b/client/src/components/navbar/ChangeNameBtn.tsx index 874f55d..4eac7b4 100644 --- a/client/src/components/navbar/ChangeNameBtn.tsx +++ b/client/src/components/navbar/ChangeNameBtn.tsx @@ -1,13 +1,13 @@ import React, {useState} from 'react'; import { - Button, Dialog, DialogActions, DialogTitle, DialogContent, DialogContentText, TextField, + Button, Dialog, DialogActions, DialogTitle, DialogContent, DialogContentText, TextField, Typography, MenuItem, } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import SettingsService from '../../services/SettingsService'; export default function ChangeNameBtn(props) { - const {setpname, pname} = props; + const {setpname, pname, mobile} = props; const [tmpPrefName, setTmpPrefName] = useState(pname); const [open, setOpen] = useState(false); @@ -28,9 +28,15 @@ export default function ChangeNameBtn(props) { return (
- + { + mobile ? + + Change Name + : + + }
Change Name diff --git a/client/src/components/navbar/Navbar.tsx b/client/src/components/navbar/Navbar.tsx index 71cfe6a..a03ba7e 100644 --- a/client/src/components/navbar/Navbar.tsx +++ b/client/src/components/navbar/Navbar.tsx @@ -156,6 +156,9 @@ export default function Navbar(props) { )) } + { + userData.isAuthenticated && + } { userData.isAuthenticated && @@ -199,7 +202,7 @@ export default function Navbar(props) { { - userData.isAuthenticated && + userData.isAuthenticated && } diff --git a/client/src/components/settings/admin/ConfigSettings.tsx b/client/src/components/settings/admin/ConfigSettings.tsx index 094b98f..4456f23 100644 --- a/client/src/components/settings/admin/ConfigSettings.tsx +++ b/client/src/components/settings/admin/ConfigSettings.tsx @@ -1,7 +1,7 @@ import React, {useState, useEffect, useContext} from 'react'; import { - Button, CardContent, Typography, TextField, Grid, + Button, CardContent, Typography, TextField, Grid, Checkbox, } from '@mui/material'; import BaseCard from '../../common/cards/BaseCard'; @@ -17,12 +17,14 @@ export default function ConfigSettings(props) { const [currSem, setCurrSem] = useState(''); const [slackURL, setSlackURL] = useState(''); const [questionsURL, setQuestionsURL] = useState(''); + const [enforceCMUEmail, setEnforceCMUEmail] = useState(true); useEffect(() => { setCurrSem(adminSettings.currSem); setSlackURL(adminSettings.slackURL); + setEnforceCMUEmail(adminSettings.enforceCMUEmail); setQuestionsURL(queueData.questionsURL); - }, [adminSettings]); + }, [adminSettings, queueData]); const handleUpdateSemester = (event) => { event.preventDefault(); @@ -60,6 +62,16 @@ export default function ConfigSettings(props) { ); }; + const handleUpdateCmuEmailEnabled = (event) => { + event.preventDefault(); + + SettingsService.updateEnforceCmuEmail( + JSON.stringify({ + enforceCMUEmail: enforceCMUEmail, + }), + ); + }; + return ( @@ -87,8 +99,26 @@ export default function ConfigSettings(props) { -
+ + + Enforce CMU Email: + { + setEnforceCMUEmail(e.target.checked); + }} + /> + + + + + + +
+ { diff --git a/client/src/components/settings/admin/Locations.tsx b/client/src/components/settings/admin/Locations.tsx index ae8b009..a820d10 100644 --- a/client/src/components/settings/admin/Locations.tsx +++ b/client/src/components/settings/admin/Locations.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useState, useContext, useMemo} from 'react'; import { TableCell, Typography, } from '@mui/material'; diff --git a/client/src/components/settings/admin/QueueRejoinSettings.tsx b/client/src/components/settings/admin/QueueRejoinSettings.tsx index 75caa64..a79247c 100644 --- a/client/src/components/settings/admin/QueueRejoinSettings.tsx +++ b/client/src/components/settings/admin/QueueRejoinSettings.tsx @@ -1,5 +1,5 @@ -import React, {useContext} from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import { Button, CardContent, Typography, TextField, Grid, } from '@mui/material'; @@ -11,18 +11,23 @@ import {QueueDataContext} from '../../../contexts/QueueDataContext'; export default function QueueRejoinSettings(props) { const {queueData} = useContext(QueueDataContext); + + const [rejoinTime, setRejoinTime] = useState(15); + + useEffect(() => { + setRejoinTime(queueData.rejoinTime); + }, [queueData]); + const onSubmit = (event) => { event.preventDefault(); SettingsService.updateRejoinTime( JSON.stringify({ - rejoinTime: rejoinTimeInput, + rejoinTime: rejoinTime, }), ); }; - let rejoinTimeInput = queueData.rejoinTime; - return ( @@ -39,9 +44,9 @@ export default function QueueRejoinSettings(props) { variant="standard" sx={{mx: 1, mt: -1}} style={{width: '50px'}} - defaultValue={queueData.rejoinTime ? queueData.rejoinTime : 10} + value={rejoinTime} onChange={(e) => { - rejoinTimeInput = parseInt(e.target.value, 10); + setRejoinTime(parseInt(e.target.value, 10)); }} inputProps={{min: 0, style: {textAlign: 'center'}}} /> diff --git a/client/src/components/settings/admin/TASettings.tsx b/client/src/components/settings/admin/TASettings.tsx index f7f52f8..21e1b11 100644 --- a/client/src/components/settings/admin/TASettings.tsx +++ b/client/src/components/settings/admin/TASettings.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useContext, useMemo} from 'react'; +import React, {useState, useContext, useMemo} from 'react'; import { Button, Checkbox, FormControlLabel, Grid, TableCell, TableRow, Typography, useTheme, } from '@mui/material'; diff --git a/client/src/contexts/AdminSettingsContext.tsx b/client/src/contexts/AdminSettingsContext.tsx index 22fe0b2..7acc4c4 100644 --- a/client/src/contexts/AdminSettingsContext.tsx +++ b/client/src/contexts/AdminSettingsContext.tsx @@ -14,6 +14,7 @@ const AdminSettingsContext = createContext({ adminSettings: { currSem: '', slackURL: undefined, + enforceCMUEmail: true, }, setAdminSettings: ((adminSettings: AdminSettings) => {}) as React.Dispatch>, }); @@ -27,6 +28,7 @@ const AdminSettingsContextProvider = ({children}: {children: React.ReactNode}) = const [adminSettings, setAdminSettings] = useState({ currSem: '', slackURL: undefined, + enforceCMUEmail: true, }); // Load admin settings if user is an admin diff --git a/client/src/services/MetricsService.tsx b/client/src/services/MetricsService.tsx index 3fc323d..07f65e8 100644 --- a/client/src/services/MetricsService.tsx +++ b/client/src/services/MetricsService.tsx @@ -1,6 +1,12 @@ import http from '../http-common'; class MetricsDataService { + getRankedTAs() { + return http.get('/metrics/rankedTAs'); + } + getRankedStudents() { + return http.get('/metrics/rankedStudents'); + } getHelpedStudents() { return http.get('/metrics/helpedStudents'); } diff --git a/client/src/services/SettingsService.tsx b/client/src/services/SettingsService.tsx index d6bcf3c..1b5c39a 100644 --- a/client/src/services/SettingsService.tsx +++ b/client/src/services/SettingsService.tsx @@ -60,6 +60,9 @@ class SettingsDataService { updateRejoinTime(data) { return http.post('/settings/config/rejoin/update', data); } + updateEnforceCmuEmail(data) { + return http.post('/settings/config/enforcecmuemail/update', data); + } updatePreferredName(data) { return http.post('/settings/preferredname/update', data); } diff --git a/server/controllers/login.js b/server/controllers/login.js index 024f990..c8c6495 100644 --- a/server/controllers/login.js +++ b/server/controllers/login.js @@ -57,6 +57,10 @@ exports.post_login = async (req, res) => { res.status(500); res.json({ message: "Queue has not been initialized; must log in with owner email" }); return; + } else if (adminSettings.enforceCMUEmail && !(email.endsWith("@andrew.cmu.edu") || email.endsWith("@cmu.edu"))) { + res.status(500); + res.json({ message: "Must log in with a CMU email" }); + return; } Promise.props({ diff --git a/server/controllers/metrics.js b/server/controllers/metrics.js index ac86877..4b242ae 100644 --- a/server/controllers/metrics.js +++ b/server/controllers/metrics.js @@ -1,6 +1,7 @@ const Sequelize = require('sequelize'); const Promise = require("bluebird"); const moment = require("moment-timezone"); +let settings = require('./settings'); const models = require('../models'); const { sequelize } = require('../models'); @@ -32,7 +33,8 @@ exports.get_helped_students = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null - } + }, + sem_id: settings.get_admin_settings().currSem, }, order: [['entry_time', 'DESC']] }).then((questionModels) => { @@ -76,7 +78,8 @@ exports.get_num_questions_answered = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { respond(req, res, "Got number of questions answered", { numQuestions: count }, 200); @@ -94,7 +97,8 @@ exports.get_avg_time_per_question = (req, res) => { ta_id: req.user.ta.ta_id, help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { let averageTime = 0; @@ -119,8 +123,9 @@ exports.get_num_questions_today = (req, res) => { models.question.findAndCountAll({ where: { entry_time: { - [Sequelize.Op.gte]: today - 4 * 60 * 60 * 1000, - } + [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count}) => { respond(req, res, "Got number of questions today", { numQuestionsToday: count }, 200); @@ -137,11 +142,12 @@ exports.get_num_bad_questions_today = (req, res) => { models.question.findAndCountAll({ where: { entry_time: { - [Sequelize.Op.gte]: today - 4 * 60 * 60 * 1000, + [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), }, num_asked_to_fix: { [Sequelize.Op.gt]: 0 - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count}) => { respond(req, res, "Got number of bad questions today", { numBadQuestionsToday: count }, 200); @@ -157,11 +163,12 @@ exports.get_avg_wait_time_today = (req, res) => { models.question.findAndCountAll({ where: { entry_time: { - [Sequelize.Op.gte]: today - 4 * 60 * 60 * 1000, + [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), }, help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { @@ -187,8 +194,9 @@ exports.get_ta_student_ratio_today = (req, res) => { models.question.findAndCountAll({ where: { entry_time: { - [Sequelize.Op.gte]: today - 4 * 60 * 60 * 1000, - } + [Sequelize.Op.gte]: new Date(today - 8 * 60 * 60 * 1000), + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { const taCount = rows.reduce((acc, questionModel) => { @@ -227,6 +235,10 @@ exports.get_total_num_questions = (req, res) => { models.question.findAndCountAll({ where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, } }).then(({count}) => { respond(req, res, "Got number of questions answered", { numQuestions: count }, 200); @@ -243,7 +255,8 @@ exports.get_total_avg_time_per_question = (req, res) => { where: { help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { let averageTime = 0; @@ -269,7 +282,8 @@ exports.get_total_avg_wait_time = (req, res) => { where: { help_time: { [Sequelize.Op.ne]: null, - } + }, + sem_id: settings.get_admin_settings().currSem, } }).then(({count, rows}) => { @@ -294,15 +308,19 @@ exports.get_num_students_per_day_last_week = (req, res) => { models.question.findAll({ attributes: [ - [Sequelize.fn('date', Sequelize.col('entry_time')), 'day'], + [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`)), 'day'], [Sequelize.fn('count', Sequelize.col('question_id')), 'count'] ], where: { entry_time: { - [Sequelize.Op.gte]: today - 7 * 24 * 60 * 60 * 1000, - } + [Sequelize.Op.gte]: new Date(today - 7 * 24 * 60 * 60 * 1000), + }, + help_time: { + [Sequelize.Op.ne]: null + }, + sem_id: settings.get_admin_settings().currSem, }, - group: [Sequelize.fn('date', Sequelize.col('entry_time'))], + group: [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('day'), 'ASC']] }).then((data) => { let numStudentsPerDayLastWeek = []; @@ -325,17 +343,24 @@ exports.get_num_students_per_day = (req, res) => { models.question.findAll({ attributes: [ - [Sequelize.fn('date_trunc', 'day', Sequelize.col('entry_time')), 'day_of_week'], + [Sequelize.fn('date_part', 'dow', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`)), 'day_of_week'], [Sequelize.fn('count', Sequelize.col('question_id')), 'count'] ], - group: [Sequelize.fn('date_trunc', 'day', Sequelize.col('entry_time')), 'day_of_week'], + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, + }, + group: [Sequelize.fn('date_part', 'dow', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('count'), 'DESC']] }).then((data) => { let numStudentsPerDay = []; + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; for (const row of data) { let datecount = row.dataValues; - datecount.day_of_week = new Date(datecount.day_of_week).toLocaleDateString('en-US', {weekday : 'long'}); + datecount.day_of_week = days[datecount.day_of_week]; numStudentsPerDay.push({'day': datecount.day_of_week, 'students': datecount.count}); } @@ -351,10 +376,16 @@ exports.get_num_students_overall = (req, res) => { models.question.findAll({ attributes: [ - [Sequelize.fn('date', Sequelize.col('entry_time')), 'day'], + [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`)), 'day'], [Sequelize.fn('count', Sequelize.col('question_id')), 'count'] ], - group: [Sequelize.fn('date', Sequelize.col('entry_time'))], + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + }, + }, + group: [Sequelize.fn('date', Sequelize.literal(`"entry_time" AT TIME ZONE 'EST'`))], order: [[Sequelize.col('day'), 'ASC']] }).then((data) => { let numStudentsOverall = []; @@ -367,3 +398,135 @@ exports.get_num_students_overall = (req, res) => { respond(req, res, "Got number of students overall", { numStudentsOverall: numStudentsOverall }, 200); }); } + +exports.get_ranked_students = (req, res) => { + if (!req.user || !req.user.isTA || !req.user.isAdmin) { + respond_error(req, res, "You don't have permission to perform this operation", 403); + return; + } + + let studentMap = {}; + models.question.findAll({ + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + } + } + }).then((questionModels) => { + + + for (const questionModel of questionModels) { + let question = questionModel.dataValues; + + if (question.student_id in studentMap) { + studentMap[question.student_id].count++; + studentMap[question.student_id].timeHelped += (question.exit_time - question.help_time) / 1000 / 60; + studentMap[question.student_id].badCount += parseInt(question.num_asked_to_fix); + } else { + studentMap[question.student_id] = { + count: 1, + timeHelped: (question.exit_time - question.help_time) / 1000 / 60, + badCount: parseInt(question.num_asked_to_fix) + }; + } + } + + let accountReqs = []; + for (const student_id in studentMap) { + accountReqs.push(models.account.findByPk(student_id)); + } + + return Promise.all(accountReqs); + }).then((accounts) => { + let rankedStudents = []; + + for (const account of accounts) { + let accountData = account.dataValues; + let user_id = accountData.user_id; + + if (user_id in studentMap) { + rankedStudents.push({ + student_name: accountData.preferred_name, + student_andrew: accountData.email.split("@")[0], + count: studentMap[user_id].count, + badCount: studentMap[user_id].badCount, + timeHelped: Math.round(studentMap[user_id].timeHelped * 10) / 10, + }); + } + } + + rankedStudents.sort((a, b) => { + if (a.count != b.count) { + return b.count - a.count; + } else { + return b.timeHelped - a.timeHelped; + } + }); + respond(req, res, "Got ranked students", { rankedStudents: rankedStudents }, 200); + }); +} + +exports.get_ranked_tas = (req, res) => { + if (!req.user || !req.user.isTA || !req.user.isAdmin) { + respond_error(req, res, "You don't have permission to perform this operation", 403); + return; + } + + let taMap = {}; + models.question.findAll({ + where: { + sem_id: settings.get_admin_settings().currSem, + help_time: { + [Sequelize.Op.ne]: null + } + } + }).then((questionModels) => { + + for (const questionModel of questionModels) { + let question = questionModel.dataValues; + + if (question.ta_id in taMap) { + taMap[question.ta_id].count++; + taMap[question.ta_id].timeHelping += (question.exit_time - question.help_time) / 1000 / 60; + } else { + taMap[question.ta_id] = { + count: 1, + timeHelping: (question.exit_time - question.help_time) / 1000 / 60, + }; + } + } + + let accountReqs = []; + for (const ta_id in taMap) { + accountReqs.push(models.account.findByPk(ta_id)); + } + + return Promise.all(accountReqs); + }).then((accounts) => { + let rankedTAs = []; + + for (const account of accounts) { + let accountData = account.dataValues; + let user_id = accountData.user_id; + + if (user_id in taMap) { + rankedTAs.push({ + ta_name: accountData.preferred_name, + ta_andrew: accountData.email.split("@")[0], + count: taMap[user_id].count, + timeHelping: Math.round(taMap[user_id].timeHelping * 10) / 10, + }); + } + } + + rankedTAs.sort((a, b) => { + if (a.count != b.count) { + return b.count - a.count; + } else { + return b.timeHelped - a.timeHelped; + } + }); + respond(req, res, "Got ranked TAs", { rankedTAs: rankedTAs }, 200); + }); +} diff --git a/server/controllers/settings.js b/server/controllers/settings.js index 1551df9..fd3561d 100644 --- a/server/controllers/settings.js +++ b/server/controllers/settings.js @@ -11,14 +11,56 @@ const home = require('./home'); // Global admin settings // FIXME: some default values are set to simplify testing; // In production, these should be cleared -let adminSettings = { +var fs = require('fs'); +const defaultAdminSettings = { currSem: "S23", slackURL: null, questionsURL: '', - rejoinTime: 10 + rejoinTime: 15, + enforceCMUEmail: true, + dayDictionary: {} }; +let adminSettings = defaultAdminSettings + +function haveSameKeys(obj1, obj2) { + const obj1Keys = Object.keys(obj1).sort(); + const obj2Keys = Object.keys(obj2).sort(); + return JSON.stringify(obj1Keys) === JSON.stringify(obj2Keys); +} + +// If no admin setting have been generated, use the above default values +if (!fs.existsSync('../adminSettings.json')) { + var json = JSON.stringify(adminSettings) + fs.writeFile('../adminSettings.json', json, 'utf8', function () { + console.log('Created admin settings JSON'); + }); +} +// If admin settings have been generated, but the keys don't match, update the missing keys +else if (fs.existsSync("../adminSettings.json") && !haveSameKeys(adminSettings, JSON.parse(fs.readFileSync('../adminSettings.json', 'utf8')))) { + let currAdminSettings = fs.readFileSync('../adminSettings.json', 'utf8', flag = 'r+'); + let newAdminSettings = JSON.parse(currAdminSettings); + for (let key in adminSettings) { + if (!newAdminSettings.hasOwnProperty(key)) { + newAdminSettings[key] = adminSettings[key]; + } + } + var json = JSON.stringify(newAdminSettings) + fs.writeFileSync('../adminSettings.json', json, 'utf8', function () { + console.log('Updated admin settings JSON'); + }); +} + exports.get_admin_settings = function () { + + let data = fs.readFileSync('../adminSettings.json', 'utf8', flag = 'r+'); + if (data) { + adminSettings = JSON.parse(data); + } else { + console.log('No admin settings found'); + adminSettings = defaultAdminSettings + } + return adminSettings; } @@ -42,7 +84,7 @@ function respond(req, res, message, data, status) { } function respond_success(req, res, message = null) { - if (!req.user || !req.user.isTA) { + if (!req.user) { respond_error(req, res, "You don't have permissions to view this page", 404); return; } @@ -50,6 +92,13 @@ function respond_success(req, res, message = null) { respond(req, res, message, {}, 200); } +function writeAdminSettings (settings) { + var json = JSON.stringify(settings) + fs.writeFileSync('../adminSettings.json', json, 'utf8', function () { + return; + }); +} + /** General Settings **/ exports.post_update_video_chat = function (req, res) { if (!req.user || !req.user.isTA) { @@ -178,6 +227,7 @@ exports.post_update_semester = function (req, res) { } }).then(function (results) { adminSettings.currSem = results[0].sem_id; + writeAdminSettings(adminSettings); respond_success(req, res, `Current semester set to ${sem_id} successfully`); }).catch(err => { message = err.message || "An error occurred while updating current semester"; @@ -200,6 +250,7 @@ exports.post_update_slack_url = function (req, res) { if (adminSettings.slackURL == slackURL) return; adminSettings.slackURL = slackURL; + writeAdminSettings(adminSettings); slack.update_slack(); respond_success(req, res, `Slack Webhook URL updated successfully`); } @@ -219,6 +270,7 @@ exports.post_update_questions_url = function (req, res) { if (adminSettings.questionsURL == questionsURL) return; adminSettings.questionsURL = questionsURL; + writeAdminSettings(adminSettings); home.emit_new_queue_data(); respond_success(req, res, `Questions Guide URL updated successfully`); } @@ -238,10 +290,31 @@ exports.post_update_rejoin_time = function (req, res) { if (adminSettings.rejoinTime == rejoinTime) return; adminSettings.rejoinTime = rejoinTime; + writeAdminSettings(adminSettings); home.emit_new_queue_data(); respond_success(req, res, `Rejoin time updated successfully to ${rejoinTime} minutes`); } +exports.post_update_enforce_cmu_email = function (req, res) { + if (!req.user || !req.user.isAdmin) { + respond_error(req, res, "You don't have permissions to perform this operation", 403); + return; + } + + var enforceCMUEmail = req.body.enforceCMUEmail; + + if (enforceCMUEmail == null || enforceCMUEmail == undefined) { + respond_error(req, res, "Invalid/missing parameters in request", 400); + return; + } + + if (adminSettings.enforceCMUEmail == enforceCMUEmail) return; + + adminSettings.enforceCMUEmail = enforceCMUEmail; + writeAdminSettings(adminSettings); + respond_success(req, res, `Enforcing CMU email updated successfully to: ${enforceCMUEmail}`); +} + /** Topics Functions **/ exports.post_create_topic = function (req, res) { if (!req.user || !req.user.isAdmin) { @@ -714,32 +787,35 @@ exports.post_upload_ta_csv = function (req, res) { } /* BEGIN LOCATIONS */ -let dayDictionary = {} // invariant: rooms are held at -1 to make sure they appear in the options, but could be empty days // when removing a room, need to remove it from -1 as well // mapping is 1-to-1 where Sunday = 0... Saturday = 6 const dayToRoomDictionary = (obj) => { - return Object.entries(obj).reduce((ret, entry) => { - const [key, rooms] = entry; - for (let roomIdx in rooms) { - let room = rooms[roomIdx] - if (ret[room]) { - // seen before - let keyInt = parseInt(key) - if (keyInt != null) { - ret[room].push(keyInt) - } - } else { - let keyInt = parseInt(key) - if (keyInt != null) { - ret[room] = [keyInt] + if (obj) { + return Object.entries(obj).reduce((ret, entry) => { + const [key, rooms] = entry; + for (let roomIdx in rooms) { + let room = rooms[roomIdx] + if (ret[room]) { + // seen before + let keyInt = parseInt(key) + if (keyInt != null) { + ret[room].push(keyInt) + } + } else { + let keyInt = parseInt(key) + if (keyInt != null) { + ret[room] = [keyInt] + } } } - } - return ret; - }, {}) + return ret; + }, {}) + } else { + return {} + } } exports.add_location = function (req, res) { @@ -754,12 +830,13 @@ exports.add_location = function (req, res) { return; } - if (dayDictionary["-1"]) { - dayDictionary["-1"].push(room) + if (adminSettings.dayDictionary["-1"]) { + adminSettings.dayDictionary["-1"].push(room) } else { - dayDictionary["-1"] = [room] + adminSettings.dayDictionary["-1"] = [room] } + writeAdminSettings(adminSettings); home.emit_new_queue_data(); respond_success(req, res, `Location added successfully`); } @@ -778,7 +855,7 @@ exports.post_update_locations = function (req, res) { return; } - var newDayDictionary = dayDictionary + var newDayDictionary = adminSettings.dayDictionary for (var day in daysOfWeek) { if (days.includes(daysOfWeek[day])) { // day is selected for room @@ -801,16 +878,17 @@ exports.post_update_locations = function (req, res) { } } } - dayDictionary = newDayDictionary + adminSettings.dayDictionary = newDayDictionary + writeAdminSettings(adminSettings); home.emit_new_queue_data(); respond_success(req, res, `Location changed successfully`); } exports.internal_get_locations = function () { return { - dayDictionary: dayDictionary, - roomDictionary: dayToRoomDictionary(dayDictionary) + dayDictionary: adminSettings.dayDictionary, + roomDictionary: dayToRoomDictionary(adminSettings.dayDictionary) } } @@ -830,25 +908,26 @@ exports.remove_location = function (req, res) { for (dayIdx in days) { if (dayIdx && dayIdx != null) { let dayInt = days[dayIdx] - if (!dayDictionary[dayInt].includes(room)) { + if (!adminSettings.dayDictionary[dayInt].includes(room)) { console.log("hmm shouldn't really have a day selected for this room") } else { - let roomArrForDay = dayDictionary[dayInt] + let roomArrForDay = adminSettings.dayDictionary[dayInt] // safe because room is in roomArrForDay so idx >= 0 roomArrForDay.splice(roomArrForDay.indexOf(room), 1) - dayDictionary[dayInt] = roomArrForDay + adminSettings.dayDictionary[dayInt] = roomArrForDay } } } // REMOVE ROOM FROM -1!! - let emptyRoomArr = dayDictionary["-1"] + let emptyRoomArr = adminSettings.dayDictionary["-1"] let idx = emptyRoomArr.indexOf(room) if (idx >= 0) { emptyRoomArr.splice(idx, 1) } - dayDictionary["-1"] = emptyRoomArr + adminSettings.dayDictionary["-1"] = emptyRoomArr + writeAdminSettings(adminSettings); home.emit_new_queue_data(); respond_success(req, res, `Location removed successfully`); } diff --git a/server/routes/metrics.js b/server/routes/metrics.js index 3f58280..ecec056 100644 --- a/server/routes/metrics.js +++ b/server/routes/metrics.js @@ -16,4 +16,6 @@ router.get('/totalAvgWaitTime', metrics.get_total_avg_wait_time); router.get('/numStudentsPerDayLastWeek', metrics.get_num_students_per_day_last_week); router.get('/numStudentsPerDay', metrics.get_num_students_per_day); router.get('/numStudentsOverall', metrics.get_num_students_overall); +router.get('/rankedStudents', metrics.get_ranked_students); +router.get('/rankedTAs', metrics.get_ranked_tas); module.exports = router; diff --git a/server/routes/settings.js b/server/routes/settings.js index a41c9f4..7b3c12f 100644 --- a/server/routes/settings.js +++ b/server/routes/settings.js @@ -25,6 +25,7 @@ router.post('/config/sem/update', settings.post_update_semester); router.post('/config/slack/update', settings.post_update_slack_url); router.post('/config/questions/update', settings.post_update_questions_url); router.post('/config/rejoin/update', settings.post_update_rejoin_time); +router.post('/config/enforcecmuemail/update', settings.post_update_enforce_cmu_email); router.post('/locations/update', settings.post_update_locations); router.post('/locations/add', settings.add_location); diff --git a/types/AdminSettings.ts b/types/AdminSettings.ts index ce94a0f..cb35a88 100644 --- a/types/AdminSettings.ts +++ b/types/AdminSettings.ts @@ -4,4 +4,5 @@ export type AdminSettings = { currSem: string, slackURL: string | undefined, + enforceCMUEmail: boolean, }