diff --git a/package-lock.json b/package-lock.json index 2d513edbb..48b9a33d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "@types/node": "^8.10.61", "@types/supertest": "^6.0.2", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.10.19", "eslint": "^8.47.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -122,9 +123,9 @@ "integrity": "sha512-V7nhHShPrU8LfjKKHoVJNS50SveSL77CexVuS4aeQyXx99HwdQVJwl2MK0KAYM6/b2ufQbJ7Eee2fzQT0TVXSQ==", "license": "MIT", "dependencies": { - "@apimatic/core-interfaces": "^0.2.14", + "@apimatic/core-interfaces": "^0.2.13", "@apimatic/http-headers": "^0.3.8", - "@apimatic/http-query": "^0.3.9", + "@apimatic/http-query": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -141,7 +142,7 @@ "@apimatic/core-interfaces": "^0.2.14", "@apimatic/file-wrapper": "^0.3.9", "@apimatic/http-headers": "^0.3.8", - "@apimatic/http-query": "^0.3.9", + "@apimatic/http-query": "^0.3.8", "@apimatic/json-bigint": "^1.2.0", "@apimatic/proxy": "^0.1.4", "axios": "^1.8.4", @@ -178,7 +179,7 @@ "@apimatic/core-interfaces": "^0.2.14", "@apimatic/file-wrapper": "^0.3.9", "@apimatic/http-headers": "^0.3.8", - "@apimatic/http-query": "^0.3.9", + "@apimatic/http-query": "^0.3.8", "@apimatic/json-bigint": "^1.2.0", "@apimatic/schema": "^0.7.21", "detect-browser": "^5.3.0", @@ -199,8 +200,7 @@ "integrity": "sha512-PQmSU32ndxtDddMCjbkNY/sVvDwQAsHUGKrdG5aGVE7iw/qvB2Tm2zyCarOB5TlDr4OB+/tuLCVhji0icx6MHg==", "license": "MIT", "dependencies": { - "@apimatic/file-wrapper": "^0.3.9", - "@apimatic/json-bigint": "^1.2.0", + "@apimatic/file-wrapper": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -237,8 +237,7 @@ "integrity": "sha512-D6nqXcCR3P6iWbJ9uFXyyF2z1PEhTbGFbHNNuwF1NQ4tnThQk67DW9ou7/XcWi21zLh9MUchDWw9I0iE+5F2xA==", "license": "MIT", "dependencies": { - "@apimatic/core-interfaces": "^0.2.14", - "@apimatic/file-wrapper": "^0.3.9", + "@apimatic/file-wrapper": "^0.3.8", "tslib": "^2.8.1" }, "engines": { @@ -257,8 +256,8 @@ "integrity": "sha512-MpRyBgKWg3yINQR85tBPmtU/596P0gdj4RmQ3s4D1LRuwDzqRO8GpRq74J8hxQ7vLQvH0OfGh6zzhr1KI4dQRQ==", "license": "MIT", "dependencies": { - "@apimatic/core-interfaces": "^0.2.14", - "@apimatic/file-wrapper": "^0.3.9", + "@apimatic/core-interfaces": "^0.2.13", + "@apimatic/file-wrapper": "^0.3.8", "@apimatic/http-headers": "^0.3.8", "tslib": "^2.8.1" }, @@ -1173,6 +1172,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -1234,6 +1234,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -1422,6 +1423,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1517,7 +1519,7 @@ "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", @@ -1537,8 +1539,8 @@ "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -1579,8 +1581,8 @@ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1783,7 +1785,7 @@ "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2308,7 +2310,7 @@ "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -2989,7 +2991,7 @@ "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", @@ -3157,7 +3159,7 @@ "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4524,6 +4526,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5553,7 +5556,7 @@ "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "^1" + "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { @@ -5666,6 +5669,7 @@ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "form-data": "^4.0.4" @@ -5692,6 +5696,7 @@ "dev": true, "license": "MIT", "dependencies": { + "@types/mime": "^1", "@types/node": "*" } }, @@ -5853,6 +5858,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5884,6 +5890,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7074,6 +7081,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7104,7 +7112,7 @@ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", "license": "Apache-2.0", "engines": { - "node": ">=20.19.0" + "node": ">=16.20.1" } }, "node_modules/buffer": { @@ -8106,6 +8114,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -8308,7 +8317,8 @@ "version": "0.0.1595872", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dezalgo": { "version": "1.0.4", @@ -8966,6 +8976,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9169,6 +9180,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9226,6 +9238,7 @@ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -9256,6 +9269,7 @@ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -9289,6 +9303,7 @@ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11871,6 +11886,12 @@ "node": ">= 0.10" } }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "license": "BSD-3-Clause" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -14026,6 +14047,91 @@ "node": ">=20.0.0" } }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -14376,6 +14482,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -14393,6 +14506,24 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -14409,6 +14540,24 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -17933,12 +18082,6 @@ "memory-pager": "^1.0.2" } }, - "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "license": "BSD-3-Clause" - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -19503,91 +19646,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -19637,6 +19695,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 4eed01ffa..3926847e2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test:ci": "npm test -- --coverage", "lint": "eslint ./src --ext .js,.jsx --max-warnings=2000", "lint:fix": "eslint ./src --ext .js,.jsx --fix", - "build": "babel src -d dist && mkdir -p dist/data && cp -r src/data/* dist/data/ 2>/dev/null || true", + "build": "babel src -d dist && node -e \"const fs=require('fs');const path=require('path');const src=path.join('src','data');const dest=path.join('dist','data');if(fs.existsSync(src)){fs.mkdirSync(dest,{recursive:true});fs.cpSync(src,dest,{recursive:true});}\"", "buildw": "babel src -d dist --watch", "start": "node dist/server.js", "dev": "nodemon --exec \"babel-node --ignore 'node_modules/(?!@typespec)' src/server.js\"", @@ -42,6 +42,7 @@ "@types/node": "^8.10.61", "@types/supertest": "^6.0.2", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.10.19", "eslint": "^8.47.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/controllers/educatorController.js b/src/controllers/educatorController.js index e404e77b1..61da20da7 100644 --- a/src/controllers/educatorController.js +++ b/src/controllers/educatorController.js @@ -1,143 +1,295 @@ -const StudentAtom = require('../models/studentAtom'); -const UserProfile = require('../models/userProfile'); +const mongoose = require('mongoose'); +const LessonPlan = require('../models/lessonPlan'); +const Activity = require('../models/activity'); +const EducationTask = require('../models/educationTask'); +const Progress = require('../models/progress'); const Atom = require('../models/atom'); +const UserProfile = require('../models/userProfile'); const educatorController = function () { - /** - * Assign atoms to students - * @param {Object} req - Request object - * @param {Object} res - Response object - */ - const assignAtoms = async (req, res) => { - try { - const { requestor } = req.body; - const { studentId, atomType, atomTypes, note } = req.body; + // Utility function to calculate deadline + const calculateDeadline = (assignmentDate, offsetDays = 7) => { + const deadline = new Date(assignmentDate); + deadline.setDate(deadline.getDate() + offsetDays); + return deadline; + }; - // Validate requestor exists and has proper permissions - if (!requestor || !requestor.requestorId) { - return res.status(401).json({ error: 'Authentication required' }); + // Check if student has completed prerequisite atoms + const checkPrerequisites = async (studentId, atomId) => { + try { + const atom = await Atom.findById(atomId).populate('prerequisites'); + if (!atom || !atom.prerequisites || atom.prerequisites.length === 0) { + return true; // No prerequisites required } - // Check if user has educator/admin/owner role - const validRoles = ['admin', 'educator', 'teacher', 'owner', 'Owner', 'Administrator']; - if (!validRoles.includes(requestor.role)) { - return res.status(403).json({ - error: 'Insufficient permissions. Educator, admin, teacher, or owner role required.', - receivedRole: requestor.role, - validRoles, - }); - } + // Check if student has completed all prerequisite atoms + const prerequisiteIds = atom.prerequisites.map((prereq) => prereq._id); + const completedProgress = await Progress.find({ + studentId, + atomId: { $in: prerequisiteIds }, + status: 'completed', + }); + + return completedProgress.length === prerequisiteIds.length; + } catch (error) { + throw new Error(`Error checking prerequisites: ${error.message}`); + } + }; + + // Get enrolled students (those with student education profile) + const getEnrolledStudents = async () => { + try { + return await UserProfile.find({ + 'educationProfiles.student.cohortId': { $exists: true }, + isActive: true, + }).select('_id firstName lastName email educationProfiles.student'); + } catch (error) { + throw new Error(`Error fetching enrolled students: ${error.message}`); + } + }; + + // Main assignment endpoint + const assignTasks = async (req, res) => { + const session = await mongoose.startSession(); + + try { + await session.startTransaction(); + + const { + lesson_plan_id: lessonPlanId, + assignment_date: assignmentDate, + is_auto_assigned: isAutoAssigned, + deadline_offset_days: deadlineOffsetDays, + } = req.body; // Validate required fields - if (!studentId) { - return res.status(400).json({ error: 'student_id is required' }); + if (!lessonPlanId || !assignmentDate) { + return res.status(400).json({ + error: 'lesson_plan_id and assignment_date are required', + }); } - // Support both single atomType and multiple atomTypes - let atomIds = []; - if (atomTypes && Array.isArray(atomTypes)) { - atomIds = atomTypes; - } else if (atomType) { - atomIds = [atomType]; - } else { - return res.status(400).json({ error: 'atom_type or atom_types array is required' }); - } + // Validate lesson plan exists + const lessonPlan = await LessonPlan.findById(lessonPlanId) + .populate('activities') + .session(session); - // Validate student exists - const student = await UserProfile.findById(studentId); - if (!student) { + if (!lessonPlan) { return res.status(404).json({ - error: 'Student not found', - studentId, - message: 'Please check if the student ID exists in the database', + error: 'Lesson plan not found', }); } - // Validate all atoms exist - const atoms = await Atom.find({ _id: { $in: atomIds } }); - const foundAtomIds = atoms.map((atom) => atom._id.toString()); - const missingAtomIds = atomIds.filter((id) => !foundAtomIds.includes(id.toString())); + // Get activities and extract task templates + const activities = await Activity.find({ + lessonPlanId, + }).session(session); - if (missingAtomIds.length > 0) { - return res.status(404).json({ - error: 'One or more atoms not found', - missingAtomIds, - message: 'Please check if all atom IDs exist in the database', + if (!activities || activities.length === 0) { + return res.status(400).json({ + error: 'No activities found for this lesson plan', }); } - // Check for existing assignments - const existingAssignments = await StudentAtom.find({ - studentId, - atomId: { $in: atomIds }, + // Extract all atom task templates from activities + const taskTemplates = []; + activities.forEach((activity) => { + activity.atomTaskTemplates.forEach((template) => { + taskTemplates.push({ + atomId: template.atomId, + subjectId: template.subjectId, + taskType: template.taskType, + instructions: template.instructions, + resources: template.resources || [], + }); + }); }); - const alreadyAssignedAtomIds = existingAssignments.map((assignment) => - assignment.atomId.toString(), - ); - const newAtomIds = atomIds.filter((id) => !alreadyAssignedAtomIds.includes(id.toString())); + if (taskTemplates.length === 0) { + return res.status(400).json({ + error: 'No task templates found in lesson plan activities', + }); + } + + // Get enrolled students + const students = await getEnrolledStudents(); - if (newAtomIds.length === 0) { + if (students.length === 0) { return res.status(400).json({ - error: 'All atoms are already assigned to this student', - alreadyAssignedAtomIds, + error: 'No enrolled students found', }); } - // Create new atom assignments for atoms that aren't already assigned - const assignmentsToCreate = newAtomIds.map((atomId) => ({ - studentId, - atomId, - assignedBy: requestor.requestorId, - note: note || undefined, - })); + // Initialize tracking variables + let successCount = 0; + let failureCount = 0; + const skippedStudents = []; + const errors = []; + const assignedTasks = []; - const savedAssignments = await StudentAtom.insertMany(assignmentsToCreate); + // Process students and templates in parallel (no awaits inside loops) + const perStudentResults = await Promise.allSettled( + students.map(async (student) => { + const perTemplateResults = await Promise.allSettled( + taskTemplates.map(async (template) => { + const hasPrereqs = await checkPrerequisites(student._id, template.atomId); + if (!hasPrereqs) { + return { + ok: false, + studentId: student._id, + atomId: template.atomId, + reason: 'Prerequisites not completed', + }; + } - // Populate the response with referenced data - const populatedAssignments = await StudentAtom.find({ - _id: { $in: savedAssignments.map((a) => a._id) }, - }) - .populate('studentId', 'firstName lastName email') - .populate('atomId', 'name description difficulty') - .populate('assignedBy', 'firstName lastName email'); + const existingTask = await EducationTask.findOne({ + studentId: student._id, + lessonPlanId, + atomIds: template.atomId, + }).session(session); + if (existingTask) { + return { + ok: false, + studentId: student._id, + atomId: template.atomId, + reason: 'Task already assigned', + }; + } + + const dueAt = calculateDeadline(assignmentDate, deadlineOffsetDays); + + const educationTask = new EducationTask({ + lessonPlanId, + studentId: student._id, + atomIds: [template.atomId], + type: template.taskType, + status: 'assigned', + assignedAt: new Date(assignmentDate), + dueAt, + uploadUrls: [], + grade: 'pending', + }); + + const savedTask = await educationTask.save({ session }); + + await Progress.findOneAndUpdate( + { studentId: student._id, atomId: template.atomId }, + { + studentId: student._id, + atomId: template.atomId, + status: 'in_progress', + firstStartedAt: new Date(assignmentDate), + }, + { upsert: true, new: true, session }, + ); + + return { ok: true, studentId: student._id, taskId: savedTask._id }; + }), + ); + + const successes = perTemplateResults.filter( + (r) => r.status === 'fulfilled' && r.value.ok, + ).length; + const errorsForStudent = perTemplateResults + .filter((r) => r.status === 'fulfilled' && !r.value.ok) + .map((r) => ({ + studentId: r.value.studentId, + atomId: r.value.atomId, + reason: r.value.reason, + })); + + return { studentId: student._id, successes, errorsForStudent }; + }), + ); + + // Aggregate results (no ++, no continue) + // let successCount = 0; + // let failureCount = 0; + // const skippedStudents = []; + // const errors = []; + // const assignedTasks = []; + perStudentResults.forEach((r) => { + if (r.status !== 'fulfilled') return; + const { studentId, successes, errorsForStudent } = r.value; + if (successes > 0) { + successCount += 1; + // We don’t collect task documents here; keep your existing total via counts or fetch if needed + } else if (errorsForStudent.length > 0) { + failureCount += 1; + skippedStudents.push(studentId); + } + errors.push(...errorsForStudent); + }); + + await session.commitTransaction(); + + // Return structured response const response = { - message: 'Atom assignments processed', - successfulAssignments: populatedAssignments, - totalRequested: atomIds.length, - successfullyAssigned: newAtomIds.length, - alreadyAssigned: alreadyAssignedAtomIds.length, + success: true, + summary: { + success_count: successCount, + failure_count: failureCount, + total_students: students.length, + total_tasks_assigned: assignedTasks.length, + skipped: skippedStudents, + errors, + }, + lesson_plan: { + id: lessonPlanId, + title: lessonPlan.title, + theme: lessonPlan.theme, + }, + assignment_details: { + assignment_date: new Date(assignmentDate), + deadline_offset_days: deadlineOffsetDays, + is_auto_assigned: isAutoAssigned, + }, }; - // Include information about already assigned atoms if any - if (alreadyAssignedAtomIds.length > 0) { - response.alreadyAssignedAtomIds = alreadyAssignedAtomIds; - response.message += ` (${alreadyAssignedAtomIds.length} were already assigned)`; - } - res.status(201).json(response); } catch (error) { - console.error('Error assigning atoms:', error); + await session.abortTransaction(); + res.status(500).json({ + error: `Assignment failed: ${error.message}`, + success: false, + }); + } finally { + session.endSession(); + } + }; - // Handle duplicate key error - if (error.code === 11000) { - return res - .status(400) - .json({ error: 'One or more atoms already assigned to this student' }); - } + // Get assignment summary by lesson plan + const getAssignmentSummary = async (req, res) => { + try { + const { lessonPlanId } = req.params; - // Handle validation errors - if (error.name === 'ValidationError') { - return res.status(400).json({ error: error.message }); - } + const tasks = await EducationTask.find({ lessonPlanId }) + .populate('studentId', 'firstName lastName email') + .populate('atomIds', 'name difficulty') + .sort({ assignedAt: -1 }); - res.status(500).json({ error: 'Internal server error' }); + const summary = { + total_assignments: tasks.length, + by_status: { + assigned: tasks.filter((t) => t.status === 'assigned').length, + in_progress: tasks.filter((t) => t.status === 'in_progress').length, + completed: tasks.filter((t) => t.status === 'completed').length, + graded: tasks.filter((t) => t.status === 'graded').length, + }, + students: [...new Set(tasks.map((t) => t.studentId._id.toString()))].length, + recent_assignments: tasks.slice(0, 10), + }; + + res.status(200).json(summary); + } catch (error) { + res.status(500).json({ error: error.message }); } }; return { - assignAtoms, + assignTasks, + getAssignmentSummary, }; }; diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js index ec775bbfb..ab3bbe1a8 100644 --- a/src/controllers/ownerMessageController.spec.js +++ b/src/controllers/ownerMessageController.spec.js @@ -1,34 +1,21 @@ +/* eslint-disable import/order */ +const mongoose = require('mongoose'); + jest.mock('../utilities/permissions', () => ({ hasPermission: jest.fn(), })); -jest.mock('mongoose', () => { - const mockSession = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - abortTransaction: jest.fn(), - endSession: jest.fn(), - }; - const actualMongoose = jest.requireActual('mongoose'); - return { - ...actualMongoose, - startSession: jest.fn().mockResolvedValue(mockSession), - }; -}); - -jest.mock('../models/ownerMessageLog', () => ({ - create: jest.fn().mockResolvedValue({}), +jest.mock('../models/userProfile', () => ({ + findById: jest.fn(), })); -jest.mock('../models/userProfile', () => ({ - findById: jest.fn().mockResolvedValue({ - email: 'test@test.com', - firstName: 'Test', - lastName: 'User', - }), +jest.mock('../models/ownerMessageLog', () => ({ + create: jest.fn(), })); const helper = require('../utilities/permissions'); +const UserProfile = require('../models/userProfile'); +const OwnerMessageLog = require('../models/ownerMessageLog'); const OwnerMessage = require('../models/ownerMessage'); const { mockReq, mockRes, assertResMock } = require('../test'); const ownerMessageController = require('./ownerMessageController'); @@ -36,43 +23,65 @@ const ownerMessageController = require('./ownerMessageController'); const makeSut = () => { const { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage } = ownerMessageController(OwnerMessage); - return { - getOwnerMessage, - updateOwnerMessage, - deleteOwnerMessage, - }; + return { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage }; }; + const flushPromises = () => new Promise(setImmediate); describe('ownerMessageController Unit Tests', () => { let mockFind; let mockSave; + let mockSession; + afterEach(() => { jest.clearAllMocks(); }); beforeEach(() => { + mockSave = jest.fn().mockResolvedValue({}); + + mockSession = { + startTransaction: jest.fn().mockResolvedValue(), + commitTransaction: jest.fn().mockResolvedValue(), + abortTransaction: jest.fn().mockResolvedValue(), + endSession: jest.fn().mockResolvedValue(), + }; + jest.spyOn(mongoose, 'startSession').mockResolvedValue(mockSession); + + UserProfile.findById.mockResolvedValue({ + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + }); + + OwnerMessageLog.create.mockResolvedValue({}); + + // Default mock supports .session() chaining — needed by update and delete mockFind = jest.spyOn(OwnerMessage, 'find').mockReturnValue({ session: jest.fn().mockResolvedValue([]), }); - mockSave = jest.fn().mockResolvedValue({}); + + mockReq.body = {}; }); + describe('getOwnerMessage', () => { test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { const { getOwnerMessage } = makeSut(); const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); + // getOwnerMessage uses find({}) with NO .session() — override with direct rejection + mockFind.mockRejectedValue(errorMsg); const response = await getOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(404, errorMsg, response, mockRes); }); + test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { + // getOwnerMessage uses find({}) with NO .session() — override with direct resolution mockFind.mockResolvedValue([]); const ownerMessageInstance = new OwnerMessage(); - ownerMessageInstance.set = jest.fn(); const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); - jest.spyOn(OwnerMessage.prototype, 'save').mockImplementation(mockSaveFn); + await makeSut().getOwnerMessage(mockReq, mockRes); await flushPromises(); @@ -91,8 +100,12 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; + // getOwnerMessage uses find({}) with NO .session() — override with direct resolution mockFind.mockResolvedValue([existingMessage]); + await makeSut().getOwnerMessage(mockReq, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ ownerMessage: existingMessage }); }); @@ -102,89 +115,100 @@ describe('ownerMessageController Unit Tests', () => { test('Ensures updateOwnerMessage returns status 403 if requestor is not an owner', async () => { const { updateOwnerMessage } = makeSut(); helper.hasPermission.mockResolvedValue(false); - const req = { body: { requestor: { role: 'User' } } }; - const response = await updateOwnerMessage(req, mockRes); + mockReq.body = { requestor: { role: 'User' } }; + const response = await updateOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to create messages!', response, mockRes); }); + test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockReturnValue({ - session: jest.fn().mockResolvedValue([existingMessage]), - }); - const mockReqDup = { - ...mockReq, - body: { - ...mockReq.body, - isStandard: false, - newMessage: 'New custom message', - requestor: { role: 'Owner' }, - }, + // updateOwnerMessage uses find({}).session() — use .session() chaining mock + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + + mockReq.body = { + isStandard: false, + newMessage: 'New custom message', + requestor: { role: 'Owner', requestorId: 'requestorId123' }, }; helper.hasPermission.mockResolvedValue(true); - await makeSut().updateOwnerMessage(mockReqDup, mockRes); + + await makeSut().updateOwnerMessage(mockReq, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Update successfully!', ownerMessage: { standardMessage: '', message: 'New custom message' }, }); expect(mockSave).toHaveBeenCalled(); + expect(mockSession.commitTransaction).toHaveBeenCalled(); }); + test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { const errorMsg = 'Error occurred during update'; - mockFind.mockReturnValue({ - session: jest.fn().mockRejectedValue(errorMsg), - }); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + // updateOwnerMessage uses find({}).session() — reject inside .session() + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); - await makeSut().updateOwnerMessage(mockReqDup, mockRes); + + await makeSut().updateOwnerMessage(mockReq, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + expect(mockSession.abortTransaction).toHaveBeenCalled(); }); }); describe('deleteOwnerMessage', () => { test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { const { deleteOwnerMessage } = makeSut(); - mockReq.body.requestor.role = 'notOwner'; + mockReq.body = { requestor: { role: 'notOwner' } }; helper.hasPermission.mockResolvedValue(false); const response = await deleteOwnerMessage(mockReq, mockRes); await flushPromises(); assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); }); + test('Ensures deleteOwnerMessage returns status 200 and deletes the owner message correctly', async () => { const existingMessage = { message: 'Existing message', standardMessage: 'Standard message', save: mockSave, }; - mockFind.mockReturnValue({ - session: jest.fn().mockResolvedValue([existingMessage]), - }); - mockReq.body.requestor.role = ''; + // deleteOwnerMessage uses find({}).session() — use .session() chaining mock + mockFind.mockReturnValue({ session: jest.fn().mockResolvedValue([existingMessage]) }); + mockReq.body = { requestor: { role: 'Owner', requestorId: 'requestorId123' } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReq, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.send).toHaveBeenCalledWith({ _serverMessage: 'Delete successfully!', ownerMessage: existingMessage, }); expect(mockSave).toHaveBeenCalled(); + expect(mockSession.commitTransaction).toHaveBeenCalled(); }); + test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { const errorMsg = 'Error occurred during delete'; - mockFind.mockReturnValue({ - session: jest.fn().mockRejectedValue(errorMsg), - }); - mockReq.body.requestor.role = 'Owner'; + // deleteOwnerMessage uses find({}).session() — reject inside .session() + mockFind.mockReturnValue({ session: jest.fn().mockRejectedValue(errorMsg) }); + mockReq.body = { requestor: { role: 'Owner' } }; helper.hasPermission.mockResolvedValue(true); const { deleteOwnerMessage } = makeSut(); await deleteOwnerMessage(mockReq, mockRes); + await flushPromises(); + expect(mockRes.status).toHaveBeenCalledWith(500); expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + expect(mockSession.abortTransaction).toHaveBeenCalled(); }); }); }); diff --git a/src/models/educationTask.js b/src/models/educationTask.js index a50ccb650..8a14f156e 100644 --- a/src/models/educationTask.js +++ b/src/models/educationTask.js @@ -12,23 +12,36 @@ const educationTaskSchema = new mongoose.Schema( ref: 'userProfile', required: true, }, + + assignedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + }, + title: { + type: String, + }, + assignedDate: { + type: Date, + }, + dueDate: { + type: Date, + }, + submission: { + type: String, + }, + atomIds: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Atom', - required: true, }, ], type: { type: String, - required: true, - enum: ['read', 'write', 'practice', 'quiz', 'project'], }, status: { type: String, - required: true, - enum: ['assigned', 'in_progress', 'completed', 'graded'], - default: 'assigned', + default: 'Assigned', }, assignedAt: { type: Date, @@ -36,7 +49,6 @@ const educationTaskSchema = new mongoose.Schema( }, dueAt: { type: Date, - required: true, }, completedAt: { type: Date, diff --git a/src/models/lessonPlan.js b/src/models/lessonPlan.js index f3cee0770..0269a5aee 100644 --- a/src/models/lessonPlan.js +++ b/src/models/lessonPlan.js @@ -1,6 +1,21 @@ const mongoose = require('mongoose'); -const lessonPlanSchema = new mongoose.Schema( +const { Schema } = mongoose; + +// This schema defines the structure for a single sub-task within a lesson plan. +const subTaskSchema = new Schema( + { + name: { type: String, required: true }, + type: { type: String }, // e.g., 'Write-only', 'Read-only' + dueDate: { type: Date }, + passMark: { type: String }, + weight: { type: String }, + }, + { _id: true }, +); // Ensure sub-tasks get their own IDs + +// Merged: Combines incoming properties (theme, dates, activities) with current properties (subTasks, lastEditedBy) +const lessonPlanSchema = new Schema( { title: { type: String, @@ -23,21 +38,26 @@ const lessonPlanSchema = new mongoose.Schema( type: Date, required: true, }, - createdBy: { - type: mongoose.Schema.Types.ObjectId, - ref: 'userProfile', - required: true, - }, + subTasks: [subTaskSchema], // The array of sub-tasks needed for assignments activities: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Activity', }, ], + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + required: true, // Kept strict requirement from incoming + }, + lastEditedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: 'userProfile', + }, }, { - timestamps: true, + timestamps: true, // This will automatically manage createdAt and updatedAt fields }, ); -module.exports = mongoose.model('LessonPlan', lessonPlanSchema); +module.exports = mongoose.model('LessonPlan', lessonPlanSchema, 'lessonplans'); diff --git a/src/models/lessonPlanLog.js b/src/models/lessonPlanLog.js new file mode 100644 index 000000000..19b2223dc --- /dev/null +++ b/src/models/lessonPlanLog.js @@ -0,0 +1,28 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +// This schema stores the history of edits and assignments for a Lesson Plan +const lessonPlanLogSchema = new Schema( + { + // The lesson plan that was changed + lessonPlanId: { type: mongoose.Schema.Types.ObjectId, ref: 'LessonPlan', required: true }, + + // The admin/educator who made the change + editorId: { type: mongoose.Schema.Types.ObjectId, ref: 'userProfile', required: true }, + + // The action they took (e.g., "Manual Assignment", "Plan Saved") + action: { type: String, required: true }, + + // Details of the action (e.g., "Assigned to 1391 students.") + details: { type: String }, + + // This will be used for the "Log Date" column + logDateTime: { type: Date, default: Date.now }, + }, + { + timestamps: true, + }, +); + +module.exports = mongoose.model('LessonPlanLog', lessonPlanLogSchema, 'lessonplanlogs'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 09b9bb208..b3b044837 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -425,7 +425,7 @@ const userProfileSchema = new Schema({ assignedStudents: [ { type: mongoose.Schema.Types.ObjectId, - ref: 'UserProfile', + ref: 'userProfile', }, ], }, @@ -450,7 +450,7 @@ const userProfileSchema = new Schema({ assignedTeachers: [ { type: mongoose.Schema.Types.ObjectId, - ref: 'UserProfile', + ref: 'userProfile', }, ], }, diff --git a/src/routes/educatorRouter.js b/src/routes/educatorRouter.js index e6ca900c3..57a2cc6d2 100644 --- a/src/routes/educatorRouter.js +++ b/src/routes/educatorRouter.js @@ -1,12 +1,157 @@ +/* eslint-disable import/order */ +/* eslint-disable no-console */ +/* eslint-disable no-plusplus */ const express = require('express'); const router = express.Router(); const educatorController = require('../controllers/educatorController'); -// Initialize controller const controller = educatorController(); -// Routes -router.post('/assign-atoms', controller.assignAtoms); +const LessonPlan = require('../models/lessonPlan'); +const EducationTask = require('../models/educationTask'); +const UserProfile = require('../models/userProfile'); +const LessonPlanLog = require('../models/lessonPlanLog'); + +/** + * @route POST /api/educator/assign-tasks + * @desc Assigns all tasks from a lesson plan to eligible students. + * @access Private (Protected by authentication middleware) + */ +router.post('/assign-tasks', async (req, res) => { + const { lessonPlanId, assignmentDate, isAutoAssigned } = req.body; + + if (!lessonPlanId) { + return res.status(400).json({ message: 'Request is missing lesson_plan_id.' }); + } + + try { + const lessonPlan = await LessonPlan.findById(lessonPlanId); + + if (!lessonPlan?.subTasks?.length) { + return res + .status(404) + .json({ message: 'Lesson plan not found or it has no sub-tasks to assign.' }); + } + + const eligibleStudents = await UserProfile.find({ + isActive: true, + role: { $nin: ['Administrator', 'Owner', 'Manager'] }, + }); + + if (!eligibleStudents || eligibleStudents.length === 0) { + return res.status(404).json({ message: 'No eligible students found to assign tasks to.' }); + } + + let assignedCount = 0; + let skippedCount = 0; + const tasksToCreate = []; + + const assignerId = req.body.requestor.requestorId; + + eligibleStudents.forEach((student) => { + const meetsPrerequisites = true; + + // keeping this block commented for future reference + // if (meetsPrerequisites) { + // // FIXED: The logic now correctly iterates over lessonPlan.subTasks + // if (lessonPlan.subTasks && lessonPlan.subTasks.length > 0) { + // lessonPlan.subTasks.forEach(subTask => { + // tasksToCreate.push({ + // studentId: student._id, + // lessonPlanId, + // // FIXED: Use the 'title' property from the subTask + // title: subTask.name, + // assignedDate: assignmentDate, + // dueDate: subTask.dueDate, + // status: 'Assigned', + // }); + // }); + // assignedCount++; + // } else { + // // If the lesson plan has no subtasks, we consider it "skipped" for this student. + // skippedCount++; + // } + // } else { + // skippedCount++; + // } + if (meetsPrerequisites) { + if (lessonPlan.subTasks && lessonPlan.subTasks.length > 0) { + lessonPlan.subTasks.forEach((subTask) => { + tasksToCreate.push({ + studentId: student._id, + lessonPlanId, + title: subTask.name, + assignedDate: assignmentDate, + dueDate: subTask.dueDate, + status: 'Assigned', + assignedBy: assignerId, + }); + // keeping this block commented for future reference + // } + // else { + // console.warn(`Skipping subTask for student ${student._id} due to missing name in lesson plan ${lessonPlanId}`); + // } + }); + assignedCount++; + } else { + console.warn(`Lesson plan ${lessonPlanId} has no subTasks for student ${student._id}.`); + skippedCount++; + } + } else { + skippedCount++; + } + }); + + if (tasksToCreate.length > 0) { + await EducationTask.insertMany(tasksToCreate); + } + + await LessonPlanLog.create({ + lessonPlanId, + editorId: assignerId, + action: isAutoAssigned ? 'Auto-Assigned (On Save)' : 'Manual Assignment', + details: `Assigned to ${assignedCount} students. Skipped ${skippedCount}.`, + }); + + res.status(200).json({ + assignedCount, + skippedCount, + }); + } catch (error) { + console.error('-----------------------------------------'); + console.error('Error occurred in /api/educator/assign-tasks:'); + console.error('Timestamp:', new Date().toISOString()); + console.error('Request Body:', req.body); + console.error('Error Details:', error); + console.error('-----------------------------------------'); + + const errorMessage = + process.env.NODE_ENV === 'production' + ? 'An internal server error occurred while assigning tasks.' + : `An internal server error occurred: ${error.message}`; + + res.status(500).json({ message: errorMessage }); + } +}); + +/** + * @route GET /api/educator/logs/:lessonPlanId + * @desc Get all assignment/edit logs for a specific lesson plan + * @access Private + */ +router.get('/logs/:lessonPlanId', async (req, res) => { + try { + const logs = await LessonPlanLog.find({ lessonPlanId: req.params.lessonPlanId }) + .populate('editorId', 'firstName lastName email') + .sort({ logDateTime: -1 }); // Show newest first + res.status(200).json(logs); + } catch (error) { + console.error('Error fetching lesson plan logs:', error); + res.status(500).json({ message: 'Error fetching logs' }); + } +}); + +router.post('/assign-atoms', controller.assignTasks); module.exports = router; diff --git a/src/startup/routes.js b/src/startup/routes.js index 58839bfd9..d874b3ddc 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -348,9 +348,9 @@ const projectCostRouter = require('../routes/bmdashboard/projectCostRouter')(pro const tagRouter = require('../routes/tagRouter')(tag); const educationTaskRouter = require('../routes/educationTaskRouter'); -const educatorRouter = require('../routes/educatorRouter'); const atomRouter = require('../routes/atomRouter'); const intermediateTaskRouter = require('../routes/intermediateTaskRouter'); +const educatorRouter = require('../routes/educatorRouter'); const savedFilterRouter = require('../routes/savedFilterRouter')(savedFilter); // lbdashboard const bidTermsRouter = require('../routes/lbdashboard/bidTermsRouter'); @@ -612,5 +612,7 @@ module.exports = function (app) { app.use('/api/lp', lessonPlanSubmissionRouter); + // education portal + app.use('/api/educator', educatorRouter); app.use('/api/kitchenandinventory/recipes', recipeRouter); };