diff --git a/.gitignore b/.gitignore index 1578f516..b9e86c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # production /build +/dist # misc .DS_Store diff --git a/package-lock.json b/package-lock.json index d4ff031e..24a35f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@types/react-router-dom": "^5.3.3", "ajv": "^8.17.1", "axios": "^1.4.0", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.8", "chart.js": "^4.1.1", "formik": "^2.2.9", "i18next": "^23.4.6", @@ -38,9 +38,10 @@ "react-icons": "^4.9.0", "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", - "recharts": "^2.0.0", + "recharts": "^2.15.4", "redux-persist": "^6.0.0", "sass": "^1.62.1", + "sass-embedded": "^1.98.0", "save": "^2.9.0", "web-vitals": "^2.1.4", "yup": "^1.4.0" @@ -399,6 +400,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1056,6 +1063,302 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2317,9 +2620,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", "funding": [ { "type": "github", @@ -2481,6 +2784,12 @@ "node": ">=6" } }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2792,6 +3101,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3166,6 +3485,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3320,9 +3648,9 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, "node_modules/indent-string": { @@ -3353,6 +3681,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3660,6 +4011,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -3786,6 +4144,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -4484,6 +4855,15 @@ "dev": true, "license": "MIT" }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4492,17 +4872,350 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", - "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", + "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.1.5", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.98.0", + "sass-embedded-android-arm": "1.98.0", + "sass-embedded-android-arm64": "1.98.0", + "sass-embedded-android-riscv64": "1.98.0", + "sass-embedded-android-x64": "1.98.0", + "sass-embedded-darwin-arm64": "1.98.0", + "sass-embedded-darwin-x64": "1.98.0", + "sass-embedded-linux-arm": "1.98.0", + "sass-embedded-linux-arm64": "1.98.0", + "sass-embedded-linux-musl-arm": "1.98.0", + "sass-embedded-linux-musl-arm64": "1.98.0", + "sass-embedded-linux-musl-riscv64": "1.98.0", + "sass-embedded-linux-musl-x64": "1.98.0", + "sass-embedded-linux-riscv64": "1.98.0", + "sass-embedded-linux-x64": "1.98.0", + "sass-embedded-unknown-all": "1.98.0", + "sass-embedded-win32-arm64": "1.98.0", + "sass-embedded-win32-x64": "1.98.0" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.98.0.tgz", + "integrity": "sha512-6n4RyK7/1mhdfYvpP3CClS3fGoYqDvRmLClCESS6I7+SAzqjxvGG6u5Fo+cb1nrPNbbilgbM4QKdgcgWHO9NCA==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.98.0" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.98.0.tgz", + "integrity": "sha512-LjGiMhHgu7VL1n7EJxTCre1x14bUsWd9d3dnkS2rku003IWOI/fxc7OXgaKagoVzok1kv09rzO3vFXJR5ZeONQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.98.0.tgz", + "integrity": "sha512-M9Ra98A6vYJHpwhoC/5EuH1eOshQ9ZyNwC8XifUDSbRl/cGeQceT1NReR9wFj3L7s1pIbmes1vMmaY2np0uAKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.98.0.tgz", + "integrity": "sha512-WPe+0NbaJIZE1fq/RfCZANMeIgmy83x4f+SvFOG7LhUthHpZWcOcrPTsCKKmN3xMT3iw+4DXvqTYOCYGRL3hcQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.98.0.tgz", + "integrity": "sha512-zrD25dT7OHPEgLWuPEByybnIfx4rnCtfge4clBgjZdZ3lF6E7qNLRBtSBmoFflh6Vg0RlEjJo5VlpnTMBM5MQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.98.0.tgz", + "integrity": "sha512-cgr1z9rBnCdMf8K+JabIaYd9Rag2OJi5mjq08XJfbJGMZV/TA6hFJCLGkr5/+ZOn4/geTM5/3aSfQ8z5EIJAOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.98.0.tgz", + "integrity": "sha512-OLBOCs/NPeiMqTdOrMFbVHBQFj19GS3bSVSxIhcCq16ZyhouUkYJEZjxQgzv9SWA2q6Ki8GCqp4k6jMeUY9dcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.98.0.tgz", + "integrity": "sha512-03baQZCxVyEp8v1NWBRlzGYrmVT/LK7ZrHlF1piscGiGxwfdxoLXVuxsylx3qn/dD/4i/rh7Bzk7reK1br9jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.98.0.tgz", + "integrity": "sha512-axOE3t2MTBwCtkUCbrdM++Gj0gC0fdHJPrgzQ+q1WUmY9NoNMGqflBtk5mBZaWUeha2qYO3FawxCB8lctFwCtw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.98.0.tgz", + "integrity": "sha512-OBkjTDPYR4hSaueOGIM6FDpl9nt/VZwbSRpbNu9/eEJcxE8G/vynRugW8KRZmCFjPy8j/jkGBvvS+k9iOqKV3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.98.0.tgz", + "integrity": "sha512-LeqNxQA8y4opjhe68CcFvMzCSrBuJqYVFbwElEj9bagHXQHTp9xVPJRn6VcrC+0VLEDq13HVXMv7RslIuU0zmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.98.0.tgz", + "integrity": "sha512-7w6hSuOHKt8FZsmjRb3iGSxEzM87fO9+M8nt5JIQYMhHTj5C+JY/vcske0v715HCVj5e1xyTnbGXf8FcASeAIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.98.0.tgz", + "integrity": "sha512-QikNyDEJOVqPmxyCFkci8ZdCwEssdItfjQFJB+D+Uy5HFqcS5Lv3d3GxWNX/h1dSb23RPyQdQc267ok5SbEyJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.98.0.tgz", + "integrity": "sha512-E7fNytc/v4xFBQKzgzBddV/jretA4ULAPO6XmtBiQu4zZBdBozuSxsQLe2+XXeb0X4S2GIl72V7IPABdqke/vA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.98.0.tgz", + "integrity": "sha512-VsvP0t/uw00mMNPv3vwyYKUrFbqzxQHnRMO+bHdAMjvLw4NFf6mscpym9Bzf+NXwi1ZNKnB6DtXjmcpcvqFqYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz", + "integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.98.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.98.0.tgz", + "integrity": "sha512-nP/10xbAiPbhQkMr3zQfXE4TuOxPzWRQe1Hgbi90jv2R4TbzbqQTuZVOaJf7KOAN4L2Bo6XCTRjK5XkVnwZuwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.98.0.tgz", + "integrity": "sha512-/lbrVsfbcbdZQ5SJCWcV0NVPd6YRs+FtAnfedp4WbCkO/ZO7Zt/58MvI4X2BVpRY/Nt5ZBo1/7v2gYcQ+J4svQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=14.0.0" } @@ -4688,12 +5401,48 @@ "dev": true, "license": "MIT" }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -4759,19 +5508,6 @@ } } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinypool": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", @@ -4954,6 +5690,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -5627,19 +6369,6 @@ } } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vite/node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", diff --git a/package.json b/package.json index 6bec1b70..ef00d709 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@types/react-router-dom": "^5.3.3", "ajv": "^8.17.1", "axios": "^1.4.0", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.8", "chart.js": "^4.1.1", "formik": "^2.2.9", "i18next": "^23.4.6", @@ -33,9 +33,10 @@ "react-icons": "^4.9.0", "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", - "recharts": "^2.0.0", + "recharts": "^2.15.4", "redux-persist": "^6.0.0", "sass": "^1.62.1", + "sass-embedded": "^1.98.0", "save": "^2.9.0", "web-vitals": "^2.1.4", "yup": "^1.4.0" diff --git a/src/App.tsx b/src/App.tsx index 832e1837..6e96c372 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import RootLayout from "./layout/Root"; import ManageUserTypes, { loader as loadUsers } from "./pages/Administrator/ManageUserTypes"; import Assignment from "./pages/Assignments/Assignment"; import AssignmentEditor from "./pages/Assignments/AssignmentEditor"; -import { loadAssignment } from "./pages/Assignments/AssignmentUtil"; +import { loadAssignment, loadCreateTeams } from "./pages/Assignments/AssignmentUtil"; import ResponseMappings from "./pages/ResponseMappings/ResponseMappings"; import CreateTeams from "./pages/Assignments/CreateTeams"; import ViewDelayedJobs from "./pages/Assignments/ViewDelayedJobs"; @@ -19,7 +19,7 @@ import CourseEditor from "./pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "./pages/Courses/CourseUtil"; import Questionnaire from "./pages/Questionnaires/Questionnaire"; import QuestionnaireEditor from "./pages/Questionnaires/QuestionnaireEditor"; -import { loadQuestionnaire } from "./pages/Questionnaires/QuestionnaireUtils"; +import { loadQuestionnaire, loadQuestionnaireHierarchy } from "./pages/Questionnaires/QuestionnaireUtils"; import Email_the_author from "./pages/Email_the_author/email_the_author"; import Home from "./pages/Home"; import InstitutionEditor, { loadInstitution } from "./pages/Institutions/InstitutionEditor"; @@ -84,7 +84,7 @@ function App() { { path: "assignments/edit/:id/createteams", element: , - loader: loadAssignment, + loader: loadCreateTeams, }, // Assign Reviewer: no route loader (component handles localStorage/URL id) @@ -403,17 +403,17 @@ function App() { { path: "questionnaire", element: , - loader: loadQuestionnaire, }, + loader: loadQuestionnaireHierarchy, }, ], }, { path: "*", element: }, - { path: "questionnaire", element: , loader: loadQuestionnaire }, + { path: "questionnaire", element: , loader: loadQuestionnaireHierarchy }, { path: "questionnaires", element: } leastPrivilegeRole={ROLE.INSTRUCTOR} />, - loader: loadQuestionnaire, + loader: loadQuestionnaireHierarchy, }, { path: "questionnaires/new", diff --git a/src/pages/Assignments/AssignmentDelete.tsx b/src/pages/Assignments/AssignmentDelete.tsx index 338412b2..6762aad9 100644 --- a/src/pages/Assignments/AssignmentDelete.tsx +++ b/src/pages/Assignments/AssignmentDelete.tsx @@ -10,9 +10,10 @@ import { useDispatch } from "react-redux"; interface IDeleteAssignment { assignmentData: IAssignment; onClose: () => void; + onSuccess?: () => void; } -const DeleteAssignment: React.FC = ({ assignmentData, onClose }) => { +const DeleteAssignment: React.FC = ({ assignmentData, onClose, onSuccess }) => { const { data: deletedAssignment, error: assignmentError, sendRequest: deleteAssignment } = useAPI(); const [show, setShow] = useState(true); const dispatch = useDispatch(); @@ -42,6 +43,7 @@ const DeleteAssignment: React.FC = ({ assignmentData, onClose message: `Assignment ${assignmentData.name} deleted successfully!`, }) ); + if (onSuccess) onSuccess(); onClose(); } }, [deletedAssignment?.status, dispatch, onClose, assignmentData.name]); diff --git a/src/pages/Assignments/AssignmentUtil.ts b/src/pages/Assignments/AssignmentUtil.ts index d52fcdc9..6c175ff3 100644 --- a/src/pages/Assignments/AssignmentUtil.ts +++ b/src/pages/Assignments/AssignmentUtil.ts @@ -275,3 +275,132 @@ export async function loadAssignment({ params }: any) { return { ...assignmentData, questionnaires, weights: [] }; } + +interface TeamUserApi { + id: number; + name?: string; + full_name?: string; + fullName?: string; +} + +interface TeamMemberApi { + id?: number; + user?: TeamUserApi; + name?: string; + full_name?: string; + fullName?: string; +} + +interface TeamApi { + id: number; + name: string; + users?: TeamUserApi[]; + members?: TeamMemberApi[]; +} + +interface ParticipantApi { + id: number; + user?: TeamUserApi; +} + +interface CreateTeamsParticipant { + id: string | number; + username: string; + fullName?: string; + teamName?: string | null; +} + +interface CreateTeamsTeam { + id: string | number; + name: string; + members: CreateTeamsParticipant[]; +} + +interface CreateTeamsLoaderData { + contextType: 'assignment' | 'course'; + contextName: string; + initialTeams: CreateTeamsTeam[]; + initialUnassigned: CreateTeamsParticipant[]; +} + +const toCreateTeamsParticipant = (user: TeamUserApi, teamName?: string | null): CreateTeamsParticipant => ({ + id: user.id, + username: user.name || `User ${user.id}`, + fullName: user.full_name || user.fullName || user.name || `User ${user.id}`, + teamName, +}); + +const usersFromTeam = (team: TeamApi): TeamUserApi[] => { + if (Array.isArray(team.users) && team.users.length > 0) { + return team.users; + } + + if (!Array.isArray(team.members)) { + return []; + } + + return team.members + .map((member) => { + if (member.user && member.user.id) { + return member.user; + } + if (member.id) { + return { + id: member.id, + name: member.name, + full_name: member.full_name || member.fullName, + } as TeamUserApi; + } + return undefined; + }) + .filter((user): user is TeamUserApi => Boolean(user && user.id)); +}; + +export async function loadCreateTeams({ params }: { params: { id?: string } }): Promise> { + const assignmentId = Number(params.id); + if (!assignmentId) { + throw new Error('Missing assignment id for create teams loader'); + } + + const baseData = await loadAssignment({ params }); + + const [teamsResponse, participantsResponse] = await Promise.all([ + axiosClient.get(`/teams?parent_id=${assignmentId}&types=AssignmentTeam,MentoredTeam`), + axiosClient.get(`/participants/assignment/${assignmentId}`), + ]); + + const teamsData = (teamsResponse.data || []) as TeamApi[]; + const participantsData = (participantsResponse.data || []) as ParticipantApi[]; + + const initialTeams: CreateTeamsTeam[] = teamsData.map((team) => { + const users = usersFromTeam(team); + return { + id: team.id, + name: team.name, + members: users.map((user) => toCreateTeamsParticipant(user, team.name)), + }; + }); + + const assignedIds = new Set( + initialTeams.flatMap((team) => team.members.map((member) => String(member.id))), + ); + + const initialUnassigned: CreateTeamsParticipant[] = participantsData + .map((participant) => participant.user) + .filter((user): user is TeamUserApi => Boolean(user && user.id)) + .filter((user) => !assignedIds.has(String(user.id))) + .map((user) => toCreateTeamsParticipant(user, null)); + + const assignmentName = + typeof (baseData as { name?: unknown }).name === 'string' + ? ((baseData as { name: string }).name || `Assignment ${assignmentId}`) + : `Assignment ${assignmentId}`; + + return { + ...baseData, + contextType: 'assignment', + contextName: assignmentName, + initialTeams, + initialUnassigned, + }; +} diff --git a/src/pages/Courses/Course.tsx b/src/pages/Courses/Course.tsx index 79b574d6..dad8a17e 100644 --- a/src/pages/Courses/Course.tsx +++ b/src/pages/Courses/Course.tsx @@ -21,10 +21,9 @@ import { ICourseResponse as ICourse } from "../../utils/interfaces"; */ const Courses = () => { - const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses } = useAPI(); + const { error, isLoading, data: CourseResponse, sendRequest: fetchCourses, setData: setCourseResponse } = useAPI(); const { data: InstitutionResponse, sendRequest: fetchInstitutions } = useAPI(); const { data: InstructorResponse, sendRequest: fetchInstructors } = useAPI(); - const { data: assignmentResponse, sendRequest: fetchAssignments } = useAPI(); const auth = useSelector( (state: RootState) => state.authentication, (prev, next) => prev.isAuthenticated === next.isAuthenticated @@ -51,13 +50,11 @@ const Courses = () => { fetchCourses({ url: `/courses` }); fetchInstitutions({ url: `/institutions` }); fetchInstructors({ url: `/users` }); - fetchAssignments({ url: `/assignments` }); } }, [ fetchCourses, fetchInstitutions, fetchInstructors, - fetchAssignments, location, showDeleteConfirmation.visible, auth.user.id, @@ -102,14 +99,31 @@ const Courses = () => { [] ); -const renderSubComponent = useCallback(({ row }: { row: TRow }) => { - return ( - - ); - }, []); + const onAssignmentDelete = useCallback((courseId: number, assignmentId: number) => { + if (CourseResponse?.data) { + const updatedData = CourseResponse.data.map((course: ICourseResponse) => { + if (course.id === courseId) { + return { + ...course, + assignments: course.assignments?.filter(a => a.id !== assignmentId) + }; + } + return course; + }); + setCourseResponse({ ...CourseResponse, data: updatedData }); + } + }, [CourseResponse, setCourseResponse]); + + const renderSubComponent = useCallback(({ row }: { row: TRow }) => { + return ( + onAssignmentDelete(row.original.id, assignmentId)} + /> + ); + }, [onAssignmentDelete]); const tableColumns = useMemo( () => COURSE_COLUMNS(onEditHandle, onDeleteHandle, onTAHandle, onCopyHandle), @@ -153,15 +167,10 @@ const renderSubComponent = useCallback(({ row }: { row: TRow }) return mergedTableData; } return mergedTableData.filter( - (CourseResponse: { instructor_id: number }) => - CourseResponse.instructor_id === auth.user.id + (course: { instructor_id: number }) => + course.instructor_id === auth.user.id ); - }, [mergedTableData, loggedInUserRole]); - - const coursesWithAssignments = useMemo(() => { - if (!assignmentResponse?.data) return new Set(); - return new Set(assignmentResponse.data.map((a: any) => a.course_id)); - }, [assignmentResponse?.data]); + }, [mergedTableData, loggedInUserRole, auth.user.id]); return ( <> diff --git a/src/pages/Courses/CourseAssignments.tsx b/src/pages/Courses/CourseAssignments.tsx index 3ef88959..f1307253 100644 --- a/src/pages/Courses/CourseAssignments.tsx +++ b/src/pages/Courses/CourseAssignments.tsx @@ -1,8 +1,7 @@ import { Row as TRow } from "@tanstack/react-table"; import Table from "components/Table/Table"; import React from "react"; -import useAPI from "../../hooks/useAPI"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { assignmentColumns as getBaseAssignmentColumns } from "../Assignments/AssignmentColumns"; import { useLocation, useNavigate } from "react-router-dom"; import { IAssignmentResponse } from "../../utils/interfaces"; @@ -33,10 +32,11 @@ interface ActionHandler { interface CourseAssignmentsProps { courseId: number; courseName: string; + assignments: IAssignmentResponse[]; + onAssignmentDelete: (assignmentId: number) => void; } -const CourseAssignments: React.FC = ({ courseId, courseName }) => { - const { data: assignmentResponse, sendRequest: fetchAssignments } = useAPI(); +const CourseAssignments: React.FC = ({ courseId, courseName, assignments, onAssignmentDelete }) => { const navigate = useNavigate(); const location = useLocation(); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ @@ -151,12 +151,6 @@ const CourseAssignments: React.FC = ({ courseId, courseN ] ); - useEffect(() => { - if (!showDeleteConfirmation.visible) { - fetchAssignments({ url: `/assignments` }); - } - }, [fetchAssignments, showDeleteConfirmation.visible]); - const getAssignmentColumns = (actions: ActionHandler[]) => { let baseColumns = getBaseAssignmentColumns(() => {}, () => {}, () => {}).filter(col => !["edit", "delete", "actions"].includes(String(col.id)) @@ -208,8 +202,6 @@ const CourseAssignments: React.FC = ({ courseId, courseN return [...baseColumns, actionsColumn]; }; - const assignments = (assignmentResponse?.data || []).filter( - (assignment: any) => assignment.course_id === courseId); const columns = useMemo(() => getAssignmentColumns(actionHandlers), [actionHandlers]); return ( @@ -219,6 +211,7 @@ const CourseAssignments: React.FC = ({ courseId, courseN onAssignmentDelete(showDeleteConfirmation.data!.id)} /> )} (); const Questionnaires = () => { const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); - const [showTypeModal, setShowTypeModal] = useState(false); - - // loader option - const questionnaireData :any = useLoaderData(); - - useEffect(() => { - setShowTypeModal(false); - }, [location]); - - const [tableData, setTableData] = useState(questionnaireData); - + const questionnaireHierarchy = useLoaderData() as QuestionnaireTypeGroup[]; + const [showTypeModal, setShowTypeModal] = useState(false); + const [tableData, setTableData] = useState(questionnaireHierarchy || []); + const [selectedQuestionnaire, setSelectedQuestionnaire] = useState(null); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: QuestionnaireResponse; }>({ visible: false }); - const [selectedQuestionnaire, setSelectedQuestionnaire] = useState(null); - const { error, isLoading, data: itemsResponse, sendRequest: fetchItems } = useAPI(); + + useEffect(() => { + setShowTypeModal(false); + }, [location]); + + useEffect(() => { + setTableData(questionnaireHierarchy || []); + }, [questionnaireHierarchy]); + useEffect(() => { if (error) { dispatch(alertActions.showAlert({ variant: "danger", message: error })); } - }, [error, dispatch]); - - const onDeleteQuestionnaireHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []); + }, [dispatch, error]); const onEditHandle = useCallback( - (row: TRow) => navigate(`edit/${row.original.id}`), + (row: TRow) => navigate(`/questionnaires/edit/${row.original.id}`), [navigate] ); - const onDeleteHandle = useCallback( - (row: TRow) => { - console.log("Delete clicked:", row.original); + const onDeleteHandle = useCallback((row: TRow) => { setSelectedQuestionnaire(null); - setTimeout(() => { - setShowDeleteConfirmation({ visible: true, data: row.original }); - }, 100); - }, - [] + setShowDeleteConfirmation({ visible: true, data: row.original }); + }, []); + + const onDeleteQuestionnaireHandler = useCallback( + () => setShowDeleteConfirmation({ visible: false }), + [] ); - - - useEffect(() => { - if (error) { - dispatch(alertActions.showAlert({ variant: "danger", message: error })); - } - }, [error, dispatch]); const tableColumns = useMemo( () => questionnaireColumns(onEditHandle, onDeleteHandle), [onDeleteHandle, onEditHandle] ); + const typeColumns = useMemo( + () => [ + columnHelper.accessor("type", { + header: "Questionnaire Type", + size: 300, + }), + columnHelper.display({ + id: "count", + header: "Questionnaires", + cell: ({ row }) => row.original.questionnaires.length, + size: 140, + }), + ], + [] + ); + const handleClose = () => setShowTypeModal(false); - const handleRowClick = async (questionnaire: QuestionnaireResponse) => { - setSelectedQuestionnaire(questionnaire); - if (typeof questionnaire.id === "number") { - await fetchItems({ url: `/questionnaires/${questionnaire.id}/items` }); - } - }; + const handleQuestionnaireRowClick = useCallback( + async (questionnaire: QuestionnaireResponse) => { + setSelectedQuestionnaire(questionnaire); + if (typeof questionnaire.id === "number") { + await fetchItems({ url: `/questionnaires/${questionnaire.id}/items` }); + } + }, + [fetchItems] + ); + const renderSubComponent = useCallback( + ({ row }: { row: TRow }) => ( + + + + ), + [handleQuestionnaireRowClick, tableColumns] + ); return ( <> @@ -96,22 +127,22 @@ const Questionnaires = () => { - - - setShowTypeModal(true)} - className="d-flex align-items-center shadow-sm" - style={{ - borderRadius: "8px", - width: "48px", - height: "48px", - }} - > - - - - + + + setShowTypeModal(true)} + className="d-flex align-items-center shadow-sm" + style={{ + borderRadius: "8px", + width: "48px", + height: "48px", + }} + > + + + + {showTypeModal && ( @@ -120,149 +151,141 @@ const Questionnaires = () => { Select Questionnaire Type - setShowTypeModal(false)}/> + setShowTypeModal(false)} /> )} + row.original.questionnaires.length > 0} /> - {selectedQuestionnaire && !showDeleteConfirmation.visible && ( - setSelectedQuestionnaire(null)} - size="lg" - centered - > - - - Questionnaire for {selectedQuestionnaire.name} - - - - - Type:{" "} - {selectedQuestionnaire.questionnaire_type} - - Private:{" "} - {selectedQuestionnaire.private ? ( - -) : ( - ❌ - )} - - - - Instructor:{" "} - {selectedQuestionnaire.instructor?.name} ( - {selectedQuestionnaire.instructor?.email}) - - + + + {selectedQuestionnaire && !showDeleteConfirmation.visible && ( + setSelectedQuestionnaire(null)} + size="lg" + centered + > + + Questionnaire for {selectedQuestionnaire.name} + + + + Type: {selectedQuestionnaire.questionnaire_type} + Private:{" "} + {selectedQuestionnaire.private ? ( + + ) : ( + + ❌ + + )} + + + Instructor: {selectedQuestionnaire.instructor?.name} ( + {selectedQuestionnaire.instructor?.email}) + - Items - {isLoading ? ( + Items + {isLoading ? ( Loading items... - ) : itemsResponse?.data?.length ? ( + ) : itemsResponse?.data?.length ? ( - {itemsResponse.data.map((item: any, i: number) => ( - - {item.txt} ({item.question_type}) - {item.alternatives && ( - <> - Choices: {item.alternatives} - | - > - )} - {item.min_label && ( - <> - Scale Min: {item.min_label} - > - )} - {item.max_label && ( - <> - Max: {item.max_label} - > - )} - { - item.row_names && ( - <> - Rows: {item.row_names} - > - ) - } - { - item.col_names && ( - <> - Columns: {item.col_names} - > - ) - } - {item.weight && ( - <> - Weight: {item.weight} - > - )} - - { - item.question_type === "Criterion" && | - } - - {item.textarea_width && ( - <> - Text Area Width: {item.textarea_width} - > - )} - {item.textarea_height && ( - <> - Height: {item.textarea_height} - > - )} - {item.textbox_width && ( - <> - Text Box Width: {item.textbox_width} - > - )} - - - ))} - - + {itemsResponse.data.map((item: any, index: number) => ( + + {item.txt} ({item.question_type}) + {item.alternatives && ( + <> + Choices: {item.alternatives} + | + > + )} + {item.min_label && ( + <> + Scale Min: {item.min_label} + > + )} + {item.max_label && ( + <> + Max: {item.max_label} + > + )} + {item.row_names && ( + <> + Rows: {item.row_names} + > + )} + {item.col_names && ( + <> + Columns: {item.col_names} + > + )} + {item.weight && ( + <> + Weight: {item.weight} + > + )} + {item.question_type === "Criterion" && | } + {item.textarea_width && ( + <> + Text Area Width: {item.textarea_width} + > + )} + {item.textarea_height && ( + <> + Height: {item.textarea_height} + > + )} + {item.textbox_width && ( + <> + Text Box Width: {item.textbox_width} + > + )} + + ))} + ) : ( No items defined. )} - - -)} - + + + )} + {showDeleteConfirmation.visible && showDeleteConfirmation.data && ( - { - setShowDeleteConfirmation({ visible: false }); - setSelectedQuestionnaire(null); - }} - onDeleteSuccess={(deletedId: number) => { - setTableData(prev => prev.filter(q => q.id !== deletedId)); - }} - /> -)} + { + onDeleteQuestionnaireHandler(); + setSelectedQuestionnaire(null); + }} + onDeleteSuccess={(deletedId: number) => { + setTableData((previous) => + previous.map((group) => ({ + ...group, + questionnaires: group.questionnaires.filter( + (questionnaire) => questionnaire.id !== deletedId + ), + })) + ); + }} + /> + )} > - ); }; diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 16097f7c..02b70bbb 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -5,11 +5,56 @@ import React, { useEffect, useState } from "react"; import { useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { Col, Container, Modal, Row } from 'react-bootstrap'; import QuestionnaireForm from "./QuestionnaireForm"; -import { useSelector} from "react-redux"; -import { RootState } from "../../store/store"; - - -const QuestionnaireEditor: React.FC = ({ mode }) => { +import { useSelector} from "react-redux"; +import { RootState } from "../../store/store"; + +const displayQuestionnaireType = (questionnaireType?: string) => { + const typeMap: Record = { + ReviewQuestionnaire: "Review", + MetareviewQuestionnaire: "Metareview", + AuthorFeedbackQuestionnaire: "Author feedback", + "Author FeedbackQuestionnaire": "Author feedback", + TeammateReviewQuestionnaire: "Teammate Review", + "Teammate ReviewQuestionnaire": "Teammate Review", + SurveyQuestionnaire: "Survey", + AssignmentSurveyQuestionnaire: "Assignment survey", + "Assignment SurveyQuestionnaire": "Assignment survey", + GlobalSurveyQuestionnaire: "Global survey", + "Global SurveyQuestionnaire": "Global survey", + CourseSurveyQuestionnaire: "Course survey", + "Course SurveyQuestionnaire": "Course survey", + BookmarkRatingQuestionnaire: "Bookmark rating", + "Bookmark RatingQuestionnaire": "Bookmark rating", + QuizQuestionnaire: "Quiz", + }; + + return questionnaireType ? typeMap[questionnaireType] ?? questionnaireType : ""; +}; + +const mapFetchedItemToFormItem = (item: any) => { + const size = typeof item.size === "string" ? item.size.split(",").map((value: string) => value.trim()) : []; + const [width, height] = size; + + return { + id: item.id, + txt: item.txt, + question_type: item.question_type, + weight: item.weight ?? "", + alternatives: item.alternatives ? item.alternatives.split("|").join(", ") : "", + min_label: item.min_label ?? "", + max_label: item.max_label ?? "", + textarea_width: item.question_type === "Criterion" || item.question_type === "TextArea" ? width ?? "" : "", + textarea_height: item.question_type === "Criterion" || item.question_type === "TextArea" ? height ?? "" : "", + textbox_width: item.question_type === "TextField" ? item.size ?? "" : "", + col_names: item.col_names ?? "", + row_names: item.row_names ?? "", + seq: item.seq, + break_before: item.break_before, + _destroy: item._destroy || false, + }; +}; + +const QuestionnaireEditor: React.FC = ({ mode }) => { const token = localStorage.getItem("token"); const questionnaire :any = useLoaderData(); const [searchParams] = useSearchParams(); @@ -48,15 +93,17 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { console.log("Type:", type); - // the form values to the browser console. - const onSubmit = async (values: QuestionnaireFormValues) => { - values.instructor_id = auth.user.id; - //values.instructor = auth.user.name; - console.log("Submit:", values); - const payload = transformQuestionnaireRequest(values); - const endpoint = mode === "create" - ? "/questionnaires" - : `/questionnaires/${values.id}`; + // the form values to the browser console. + const onSubmit = async (values: QuestionnaireFormValues) => { + const normalizedValues: QuestionnaireFormValues = { + ...values, + instructor_id: mode === "create" ? auth.user.id : values.instructor_id ?? questionnaire?.instructor_id, + }; + console.log("Submit:", normalizedValues); + const payload = transformQuestionnaireRequest(normalizedValues); + const endpoint = mode === "create" + ? "/questionnaires" + : `/questionnaires/${values.id}`; try { const response = await axiosClient[mode === "create" ? "post" : "put"]( @@ -75,31 +122,15 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // initial form values - const initialValues: QuestionnaireFormValues = { - id: questionnaire?.id ?? undefined, - name: questionnaire?.name ?? "", - questionnaire_type: questionnaire?.questionnaire_type ?? type ?? "", - private: questionnaire?.private ?? false, - min_question_score: questionnaire?.min_question_score ?? 0, - max_question_score: questionnaire?.max_question_score ?? 10, - items: fetchedItems.length > 0 ? fetchedItems.map(item => ({ - id: item.id, - txt: item.txt, - question_type: item.question_type, - weight: item.weight, - alternatives: item.alternatives, - min_label: item.min_label, - max_label: item.max_label, - textarea_width: item.textarea_width, - textarea_height: item.textarea_height, - textbox_width: item.textbox_width, - col_names: item.col_names, - row_names: item.row_names, - seq: item.seq, - break_before: item.break_before, - _destroy: item._destroy || false, - })) : questionnaire?.items ?? [], - }; + const initialValues: QuestionnaireFormValues = { + id: questionnaire?.id ?? undefined, + name: questionnaire?.name ?? "", + questionnaire_type: displayQuestionnaireType(questionnaire?.questionnaire_type) || type || "", + private: questionnaire?.private ?? false, + min_question_score: questionnaire?.min_question_score ?? 0, + max_question_score: questionnaire?.max_question_score ?? 10, + items: fetchedItems.length > 0 ? fetchedItems.map(mapFetchedItemToFormItem) : questionnaire?.items ?? [], + }; return ( diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx index 5c911d53..3391a99e 100644 --- a/src/pages/Questionnaires/QuestionnaireForm.tsx +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -1,5 +1,5 @@ - import React, { useEffect, useState } from "react"; - import { Formik, Field, Form, ErrorMessage } from "formik"; + import React, { useEffect } from "react"; + import { Formik, Field, Form, ErrorMessage, getIn } from "formik"; import { Button } from 'react-bootstrap'; import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; import * as Yup from "yup"; @@ -13,9 +13,8 @@ useEffect(() => { - fetchItemTypes({ url: "/item_types" }); - console.log(itemTypes?.data); - }, [fetchItemTypes]); + fetchItemTypes({ url: "/questions/types" }); + }, [fetchItemTypes]); const itemFields = Yup.object().shape({ @@ -27,10 +26,10 @@ .nullable() .notRequired(), - alternatives: Yup.string().when("question_type", ([questionType], schema) => { - if (questionType === "dropdown" || questionType === "multiple_choice") { - return schema - .required("Options are required") + alternatives: Yup.string().when("question_type", ([questionType], schema) => { + if (questionType === "Dropdown" || questionType === "MultipleChoiceRadio") { + return schema + .required("Options are required") .test( "min-2-options", "Enter at least two options, separated by commas.", @@ -47,17 +46,17 @@ return schema.notRequired(); }), - min_label: Yup.string().when("question_type", ([question_type], schema) => { - return question_type === "scale" - ? schema.required("Minimum label is required") - : schema.notRequired(); - }), - - max_label: Yup.string().when("question_type", ([question_type], schema) => { - return question_type === "scale" - ? schema.required("Maximum label is required") - : schema.notRequired(); - }), + min_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "Scale" + ? schema.required("Minimum label is required") + : schema.notRequired(); + }), + + max_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "Scale" + ? schema.required("Maximum label is required") + : schema.notRequired(); + }), }); const validationSchema = Yup.object().shape({ @@ -70,16 +69,35 @@ }); - return ( - - + type === "TextArea" ? "Text area" : + type === "TextField" ? "Text field" : + type === "MultipleChoiceRadio" ? "Multiple choice" : + type + ); + + return ( + + - {({ values, handleChange, errors, touched }) => ( - + {({ values, handleChange, errors, touched }) => ( + {values.questionnaire_type === "Teammate Review" && ( @@ -151,35 +169,48 @@ - - Private - - - - - - - ← Min Item Score Max → - - - - - - - {/* Allows users to input a variable number of questions / items */} - t.name) as string[]) ?? []} /> - - + + Private + + + + + + + ← Min Item Score Max → + + + + + + + + + {/* Allows users to input a variable number of questions / items */} + + {typeof getIn(touched, "items") !== "undefined" && + typeof getIn(errors, "items") === "string" && ( + {getIn(errors, "items")} + )} + + Save diff --git a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx index 062887a9..5fc63669 100644 --- a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx +++ b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx @@ -22,15 +22,25 @@ interface Props { itemTypes: string[]; } -const QuestionnaireItemsFieldArray: React.FC = ({ +const QuestionnaireItemsFieldArray: React.FC = ({ values, errors, touched, itemTypes -}) => { - const [questionType, setQuestionType] = useState(""); - const [numQuestions, setNumQuestions] = useState(""); - const [showNumbers, setShowNumbers] = useState(false); +}) => { + const [questionType, setQuestionType] = useState(""); + const [numQuestions, setNumQuestions] = useState(""); + const [showNumbers, setShowNumbers] = useState(false); + + const normalizedQuestionType = (type: string) => { + const mapping: Record = { + "Text area": "TextArea", + "Text field": "TextField", + "Multiple choice": "MultipleChoiceRadio", + }; + + return mapping[type] ?? type; + }; @@ -94,9 +104,9 @@ const QuestionnaireItemsFieldArray: React.FC = ({ style={{ width: "220px" }} /> - {item.question_type === "Multiple choice" || - item.question_type === "Dropdown" ? ( - <> + {normalizedQuestionType(item.question_type) === "MultipleChoiceRadio" || + normalizedQuestionType(item.question_type) === "Dropdown" ? ( + <> = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -134,8 +145,8 @@ const QuestionnaireItemsFieldArray: React.FC = ({ > - ) : item.question_type === "Scale" ? ( - <> + ) : normalizedQuestionType(item.question_type) === "Scale" ? ( + <> = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -179,8 +191,8 @@ const QuestionnaireItemsFieldArray: React.FC = ({ > - ) : item.question_type === "Criterion" ? ( - <> + ) : normalizedQuestionType(item.question_type) === "Criterion" ? ( + <> = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -237,9 +250,9 @@ const QuestionnaireItemsFieldArray: React.FC = ({ > - ) : item.question_type === "Text field" ? (<> - + = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -267,8 +281,8 @@ const QuestionnaireItemsFieldArray: React.FC = ({ /> - >): item.question_type === "Text area" ? ( - <> + >): normalizedQuestionType(item.question_type) === "TextArea" ? ( + <> = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -306,8 +321,8 @@ const QuestionnaireItemsFieldArray: React.FC = ({ > - ) : item.question_type === "Grid" ?( - <> + ) : normalizedQuestionType(item.question_type) === "Grid" ?( + <> = ({ /> Remove Item}> - { + { const item = values.items[index]; if (item.id) { setFieldValue(`items[${index}]._destroy`, true); @@ -409,7 +425,7 @@ const QuestionnaireItemsFieldArray: React.FC = ({ id: undefined, txt: "", weight: "", - question_type: questionType, + question_type: normalizedQuestionType(questionType), break_before: 1, alternatives: "", min_label: "", diff --git a/src/pages/Questionnaires/QuestionnaireUtils.tsx b/src/pages/Questionnaires/QuestionnaireUtils.tsx index e8a2bfa4..9198751c 100644 --- a/src/pages/Questionnaires/QuestionnaireUtils.tsx +++ b/src/pages/Questionnaires/QuestionnaireUtils.tsx @@ -1,10 +1,12 @@ import axiosClient from "../../utils/axios_client"; import { IInstructor } from "../../utils/interfaces"; -export type QuestionnaireType = - | "Author feedback" - | "Teammate Review" - | "Survey" +export type QuestionnaireType = + | "Review" + | "Metareview" + | "Author feedback" + | "Teammate Review" + | "Survey" | "Assignment survey" | "Global survey" | "Course survey" @@ -12,10 +14,12 @@ export type QuestionnaireType = | "Quiz"; -export const QuestionnaireTypes: QuestionnaireType[] = [ - "Author feedback", - "Teammate Review", - "Survey", +export const QuestionnaireTypes: QuestionnaireType[] = [ + "Review", + "Metareview", + "Author feedback", + "Teammate Review", + "Survey", "Assignment survey", "Global survey", "Course survey", @@ -56,7 +60,7 @@ export interface QuestionnaireFormValues { items?: IItem[]; } -export interface QuestionnaireResponse { +export interface QuestionnaireResponse { id?: number; name: string; private:boolean; @@ -67,8 +71,13 @@ export interface QuestionnaireResponse { max_question_score: number; instructor_id: number; instructor: IInstructor; - items?: IItem[]; -} + items?: IItem[]; +} + +export interface QuestionnaireTypeGroup { + type: string; + questionnaires: QuestionnaireResponse[]; +} export interface QuestionnaireRequest { id?: number; @@ -93,15 +102,28 @@ export function getQuestionnaireTypes(quest: QuestionnaireResponse[]): string[] } -export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => { - console.log("Original Form Values:", values); - const questionnaire: QuestionnaireRequest = { - id: values.id, - name: values.name, - questionnaire_type: values.questionnaire_type.replace(/\s+/g, ""), - private: values.private, - min_question_score: values.min_question_score, - max_question_score: values.max_question_score, +export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => { + const questionnaireTypeMap: Record = { + Review: "ReviewQuestionnaire", + Metareview: "MetareviewQuestionnaire", + "Author feedback": "AuthorFeedbackQuestionnaire", + "Teammate Review": "TeammateReviewQuestionnaire", + Survey: "SurveyQuestionnaire", + "Assignment survey": "AssignmentSurveyQuestionnaire", + "Global survey": "GlobalSurveyQuestionnaire", + "Course survey": "CourseSurveyQuestionnaire", + "Bookmark rating": "BookmarkRatingQuestionnaire", + Quiz: "QuizQuestionnaire", + }; + + console.log("Original Form Values:", values); + const questionnaire: QuestionnaireRequest = { + name: values.name, + questionnaire_type: + questionnaireTypeMap[values.questionnaire_type] ?? values.questionnaire_type.replace(/\s+/g, ""), + private: values.private, + min_question_score: values.min_question_score, + max_question_score: values.max_question_score, instructor_id: values.instructor_id, items_attributes: values.items ? values.items.map((item, index) => ({ @@ -132,14 +154,20 @@ export const transformQuestionnaireResponse = (data: any): QuestionnaireFormValu }; -export async function loadQuestionnaire({ params }: any) { - if (params.id) { - const response = await axiosClient.get(`/questionnaires/${params.id}`); - return transformQuestionnaireResponse(response.data); - } else { - const response = await axiosClient.get(`/questionnaires`); - return response.data.map((q: any) => transformQuestionnaireResponse(q)); - } -} - +export async function loadQuestionnaire({ params }: any) { + if (params.id) { + const response = await axiosClient.get(`/questionnaires/${params.id}`); + return transformQuestionnaireResponse(response.data); + } + + return null; +} + +export async function loadQuestionnaireHierarchy() { + const response = await axiosClient.get(`/questionnaires/hierarchical`); + return response.data.map((group: QuestionnaireTypeGroup) => ({ + ...group, + questionnaires: group.questionnaires.map((q: any) => transformQuestionnaireResponse(q)), + })); +} diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 5454e972..f2bd47a8 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -208,6 +208,7 @@ export interface ICourseResponse{ institution: { id: number | null; name: string | null }; instructor: { id: number | null; name: string | null }; date_format_pref: string; + assignments?: IAssignmentResponse[]; } export interface ICourseRequest{
- Type:{" "} - {selectedQuestionnaire.questionnaire_type} - - Private:{" "} - {selectedQuestionnaire.private ? ( - -) : ( - ❌ - )} - -
- Instructor:{" "} - {selectedQuestionnaire.instructor?.name} ( - {selectedQuestionnaire.instructor?.email}) -
+ Type: {selectedQuestionnaire.questionnaire_type} + Private:{" "} + {selectedQuestionnaire.private ? ( + + ) : ( + + ❌ + + )} +
+ Instructor: {selectedQuestionnaire.instructor?.name} ( + {selectedQuestionnaire.instructor?.email}) +
Loading items...
No items defined.