diff --git a/.env.local b/.env.local new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/.env.local @@ -0,0 +1 @@ + diff --git a/.gitignore b/.gitignore index 10e85c1c7d2c7ccb5fe82aad28538bcb65f9449b..fe4d87fecd8a2e0fdd3d2a806c96823f7e262403 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env +.env.local # vercel .vercel diff --git a/.idea/misc.xml b/.idea/misc.xml index 639900d13c6182e452e33a3bd638e70a0146c785..6e86672130aac0e4918a028246f39139ae5a99c5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/jsconfig.json b/jsconfig.json index b8d6842d7fad0a64e5ffa846e104342233782852..879f8fc5b7d23204d856dd09d330085ff8d61e9a 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } -} +} \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 4678774e6d606704bce1897a5dab960cd798bf66..2a39b71d075e0197440120491bcfa37f29d4eb57 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,28 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ['https://res.cloudinary.com'], + }, + webpack(config) { + const fileLoaderRule = config.module.rules.find((rule) => + rule.test?.test?.('.svg'), + ) + config.module.rules.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, // *.svg?url + }, + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url + use: ['@svgr/webpack'], + }, + ) + fileLoaderRule.exclude = /\.svg$/i + return config + }, +}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index d4a9662c58821d09ebebe2ef995bf80407e9b29d..459b2227cce39d19951c37da3eba4458035b75f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,17 @@ "version": "0.1.0", "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-chartjs-2": "^5.2.0", + "react-dom": "^18", + "react-select": "^5.8.0" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", @@ -33,11 +37,1934 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", + "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.24.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.1.tgz", + "integrity": "sha512-QXp1U9x0R7tkiGB0FOk8o74jhnap0FlZ5gNkRIWdG3eP+SvMFg118e1zaWewDzgABb106QSKpVsD3Wgd8t6ifA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", + "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.5.tgz", + "integrity": "sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.5", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", + "@babel/plugin-transform-classes": "^7.24.5", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.5", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.24.5", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.5", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.5", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", + "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-syntax-jsx": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, "node_modules/@babel/runtime": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -45,6 +1972,171 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -101,6 +2193,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -218,6 +2332,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "peer": true + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -384,36 +2504,293 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "engines": { - "node": ">= 8" + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, "engines": { "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", - "dev": true - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -436,12 +2813,48 @@ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -878,6 +3291,91 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -894,6 +3392,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -905,16 +3409,48 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -949,11 +3485,22 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -997,6 +3544,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1031,6 +3590,11 @@ "node": ">= 6" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1066,6 +3630,51 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1079,6 +3688,47 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1090,6 +3740,44 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1170,6 +3858,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1218,33 +3915,107 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "dependencies": { - "path-type": "^4.0.0" + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=8" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, "node_modules/eastasianwidth": { @@ -1252,6 +4023,12 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.779", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.779.tgz", + "integrity": "sha512-oaTiIcszNfySXVJzKcjxd2YjPxziAd+GmXyb2HbidCeFo6Z88ygOT7EimlrEQhM2U08VhSrbKhLOXP0kKUCZ6g==", + "dev": true + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1270,6 +4047,26 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -1428,11 +4225,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -1917,9 +4722,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1927,6 +4732,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2041,6 +4851,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -2297,6 +5116,14 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2310,7 +5137,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2377,6 +5203,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2804,12 +5635,29 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2917,6 +5765,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2934,6 +5788,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", @@ -2942,6 +5805,17 @@ "node": "14 || >=16.14" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3106,6 +5980,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3114,6 +6004,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3306,7 +6208,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -3314,6 +6215,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3364,7 +6282,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -3564,7 +6481,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3610,6 +6526,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -3625,8 +6550,42 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } }, "node_modules/read-cache": { "version": "1.0.0", @@ -3668,11 +6627,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -3692,6 +6677,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3712,7 +6735,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -3936,6 +6958,24 @@ "node": ">=8" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4153,6 +7193,11 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4197,6 +7242,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/tailwindcss": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", @@ -4267,6 +7352,14 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4438,6 +7531,76 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4447,6 +7610,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4647,6 +7823,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yaml": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", diff --git a/package.json b/package.json index c2cd5f90454007cef6ac16c790d9977ed069a82e..f9e94e36ed36bfa864f43a911327d1743842b629 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,17 @@ }, "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-chartjs-2": "^5.2.0", + "react-dom": "^18", + "react-select": "^5.8.0" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", diff --git a/src/app/(dashboard)/assign-zone-project/AssignProject.jsx b/src/app/(dashboard)/assign-zone-project/AssignProject.jsx new file mode 100644 index 0000000000000000000000000000000000000000..55660685e12137e733a32d1c931aebc826531736 --- /dev/null +++ b/src/app/(dashboard)/assign-zone-project/AssignProject.jsx @@ -0,0 +1,366 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" +import fetchRequest from "@/app/lib/fetchRequest"; + + +const AssignProject = ({ setIsOpen, listProjects, affectations, mutateProjectAffection }) => { + const [loading, setLoading] = useState(false) + const [projects, setProjects] = useState([]) + const [zones, setZones] = useState([]) + const [ selectedDay, setSelectedDay ] = useState('') + const [ selectedWeek, setSelectedWeek ] = useState('') + const [ selectedZone, setSelectedZone ] = useState(null) + const [ selectedProject, setSelectedProject ] = useState(null) + const [ places , setPlaces ] = useState([]) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + const [ otherPlaces, setOtherPlaces ] = useState([]) + + const { toggleNotification } = useNotification() + + const attributedCollabsRef = useRef() + useEffect(() => { + const fetchProjectsandZones = async () => { + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${selectedWeek}/${selectedDay}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setNbrCollabs(0) + setPlaces([]) + if ( (data.projects && data.projects.length === 0) && (data.zones && data.zones.length === 0)){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets et de zones pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + setZones([]) + }else{ + if(data.projects && data.projects.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + }else{ + setProjects(data.projects) + } + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + setZones(data.zones) + } + } + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + toggleNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + if(selectedDay && selectedWeek) fetchProjectsandZones() + }, [selectedDay, selectedWeek]) + + + const handleZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) + if(isSuccess){ + setSelectedZone(zone_id) + setPlaces(data.places) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + const handleProjectSelection = async (e) => { + const project_id = e.target.value + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingCollabs/${project_id}`, {method: 'GET'}) + if(isSuccess){ + setSelectedProject(project_id) + setNbrCollabs(data.user_count) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setNbrCollabs(0) + } + }catch(error){ + console.log(error) + } + } + + useEffect( () => { + if(nbrCollabs > 0 && places.length > 0){ + if( nbrCollabs <= places.length){ + setCollabsAttributed(nbrCollabs) + attributedCollabsRef.current = nbrCollabs + }else{ + setCollabsAttributed(places.length) + attributedCollabsRef.current = places.length + } + } + }, [nbrCollabs, places]) + + + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + attributedCollabsRef.current = collabsAttributed + 1 + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + attributedCollabsRef.current = collabsAttributed - 1 + } + + + + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) + if(isSuccess){ + setOtherPlaces(data.places) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + useEffect( () => { + if( (collabsAttributed - places.length) <= 0 && selectedOtherZone ) { + setSelectedOtherZone(null) + setOtherPlaces([]) + } + }, [collabsAttributed]) + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject, + jour: selectedDay, + semaine: selectedWeek, + nombre_personnes: nbrCollabs, + places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, + places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, + places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + } + if( selectedOtherZone && otherPlaces.length > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, + places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + } + } + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + const newAffectations = [...affectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listProjects(newAffectations) + mutateProjectAffection(newAffectations) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const closePopup = () => { + setIsOpen() + setSelectedWeek('') + setSelectedDay('') + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setPlaces([]) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherPlaces([]) + setProjects([]) + setZones([]) + setLoading(false) + + + } + + + return ( +
+
+

Attribuer un projet Ă  une zone.

+ {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
+
+

Veuillez sélectionner une date

+
+
+ +
+
+ +
+
+
+
+
+ {(!loading && (projects && projects.length)) ? + + : +
+ Aucun projet disponible +
+ } +
+

: {nbrCollabs}

+
+
+
+ {(!loading && (zones && zones.length)) ? + + : +
+ Aucune zone disponible +
+ } +
+

: {places.length}

+
+
+ {collabsAttributed > 0 && + <> +
+ Collaborateurs attribués +
+ {collabsAttributed} +
+ + +
+
+
+
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+ restant +
+
+ {((collabsAttributed - places.length) > 0) &&
+

Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

+
+ +
+

: {otherPlaces.length}

+
+
+
} + + } +
+ +
+
+
+
+
+ ) +} + +export default AssignProject diff --git a/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx new file mode 100644 index 0000000000000000000000000000000000000000..649f94f5d491e9e87781281f842dc8636725e4ff --- /dev/null +++ b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx @@ -0,0 +1,304 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" +import AddIcon from "@/static/image/svg/add.svg" +import fetchRequest from "@/app/lib/fetchRequest"; +import Loader from '@/components/Loader/Loader' + + + +const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, fullAffectations, mutateProjectAffection }) => { + const [ selectedProject, setSelectedProject ] = useState(null) + const [ zones, setZones ] = useState([]) + const [ loading, setLoading ] = useState(false) + const [ places , setPlaces ] = useState([]) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const { toggleNotification } = useNotification() + const [ selectedZone, setSelectedZone ] = useState(null) + const [ otherPlaces, setOtherPlaces ] = useState([]) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + + console.log("fullAffectations", fullAffectations) + console.log("affectations", affectations) + + + const getZones = async (day, week) => { + console.log("day, week", day, week) + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${day}/${week}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setPlaces([]) + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + console.log("dsqqqqqdsqdqs") + setZones(data.zones) + } + + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + toggleNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + + const handleProjectSelection = (project) => { + if(selectedProject && selectedProject.project.id === project.project.id){ + setSelectedProject(null) + setZones([]) + setPlaces([]) + setCollabsAttributed(0) + return + } + console.log(project) + setSelectedProject(project) + getZones(project.jour, project.semaine) + setNbrCollabs(project.nbr_personnes_restant) + } + + + useEffect( () => { + if(nbrCollabs > 0 && places.length > 0){ + if( nbrCollabs <= places.length){ + setCollabsAttributed(nbrCollabs) + }else{ + setCollabsAttributed(places.length) + } + } + }, [nbrCollabs, places]) + + const handleSelectionZone = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.week && element.id_zone.id == zone_id).map(element => element.id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) + if(isSuccess){ + console.log(data.places) + setSelectedZone(zone_id) + setPlaces(data.places) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.semaine && element.id_zone.id == zone_id).map(element => element.id) + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) + if(isSuccess){ + setOtherPlaces(data.places) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + } + console.log("projectss", affectations) + console.log("selected project", selectedProject) + + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject.project.id, + jour: selectedProject.jour, + semaine: selectedProject.semaine, + nombre_personnes: selectedProject.nombre_personnes, + places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, + places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, + places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + } + if( selectedOtherZone && otherPlaces.length > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, + places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + } + } + console.log(finalData) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + const newAffectations = [...fullAffectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listAffectationsState(newAffectations) + mutateProjectAffection(newAffectations) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + + const closePopup = () => { + setIsOpen() + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setPlaces([]) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherPlaces([]) + setZones([]) + setLoading(false) + } + + console.log(nbrCollabs) + + return ( +
+
+

Compléter l'affectation

+ {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
+
+

Veuillez sélectionner un projet

+
+ {(affectations) && + affectations.map((element, index) => +
handleProjectSelection(element)} key={index} className={`cursor-pointer mx-auto will-change-contents min-h-8 text-sm break-words flex flex-col items-center justify-center duration-150 delay-75 border-2 py-0.5 rounded-md w-fit px-2 text-sushi-600 ${ selectedProject?.project?.id === element.project.id ? "font-semibold border-sushi-400 bg-sushi-100" : "font-medium hover:border-sushi-400 hover:bg-sushi-100 border-sushi-400 bg-white"}`}> +

Projet: {element?.project?.nom} -- Collaborateurs: {element.nbr_personnes_restant}

+

Semaine: {element.semaine} -- Jour: {element.jour}

+
+ ) + } +
+
+
+

Veuillez sélectionner une zone

+
+ {(!loading) ? + (zones && zones.length) ? + + : +
+ Aucune zone disponible +
+ : +
+ +
+ + } +
+

: {places.length}

+
+
+
+ {(collabsAttributed > 0) &&
+

Collaborateurs affectées

+
+
+ {(collabsAttributed) ? collabsAttributed : 0} +
+ + +
+
+
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
+
} + {((collabsAttributed - places.length) > 0) &&
+

Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

+
+ +
+

: {otherPlaces.length}

+
+
+
} +
+
+ +
+
+
+ ) +} + +export default CompleteAffectation diff --git a/src/app/(dashboard)/assign-zone-project/page.jsx b/src/app/(dashboard)/assign-zone-project/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..51c353754c604b197ef490a74857747bda96038f --- /dev/null +++ b/src/app/(dashboard)/assign-zone-project/page.jsx @@ -0,0 +1,338 @@ +"use client" +import React, { useEffect, useState } from 'react'; +import AddIcon from "@/static/image/svg/add.svg"; +import AssignProject from './AssignProject'; +import { useNotification } from '@/context/NotificationContext' + +import Loader from '@/components/Loader/Loader' +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; +import fetchRequest from "@/app/lib/fetchRequest"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import CompleteAffectation from './CompleteAffectation'; + + + + +const AffectingZoneProject = () => { + const [isOpen, setIsOpen] = useState(false) + const [isOpenCompleteAffectation, setIsOpenCompleteAffectation] = useState(false) + const [listProjectsAffected, setListProjectsAffected] = useState([]) + const [isLoadingListProjects, setIsLoadingListProjects] = useState(false) + const { toggleNotification } = useNotification() + const [selectedWeek, setSelectedWeek] = useState(null) + const [selectedDay, setSelectedDay] = useState(null) + const [selectedAffectaionToDelete, setSelectedAffectationToDelete] = useState(null) + const [isModalOpen, setModalOpen] = useState(false); + const [listProjectsSemiAffected, setListProjectsSemiAffected] = useState([]) + + + useEffect(() => { + + // this function is to detect if there is a project that is not fully affected and return the corresponding data to use + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.id_project.users.length + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.id_project.users.length - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.id_project.users.length - project.places_occuper, + nombre_personnes: project.id_project.users.length + }; + }); + + return result; + } + + const getListOfAffectedProjects = async () => { + setIsLoadingListProjects(true) + try { + if (selectedDay && selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedDay) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, { method: 'GET' }) + } else { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, { method: 'GET' }) + } + if (isSuccess) { + setListProjectsAffected(data) + console.log("this is our ", data); + setListProjectsSemiAffected(filterAndGroupProjects(data)) + } else { + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + setIsLoadingListProjects(false) + } catch (error) { + setIsLoadingListProjects(false) + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getListOfAffectedProjects() + }, [selectedDay, selectedWeek]) + + + const handleOpenAssignProject = () => { + setIsOpen(!isOpen) + } + + const handleOpenCompleteAffectation = () => { + setIsOpenCompleteAffectation(!isOpenCompleteAffectation) + } + + const handleDeleteAffectation = async () => { + try { + var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, { method: 'DELETE' }) + if (isSuccess) { + toggleNotification({ + visible: true, + message: "Affectation supprimer avec succès", + type: "success" + }) + const filteredProjectsAffected = listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id) + setListProjectsAffected(filteredProjectsAffected) + mutateProjectsAffectaionCheck(filteredProjectsAffected) + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "Affectation introuvable", + type: "error" + }) + } else { + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + } catch (error) { + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const handleDeleteClick = (element) => { + setSelectedAffectationToDelete(element) + setModalOpen(true); + } + + + const handleConfirmDelete = () => { + handleDeleteAffectation(); + setModalOpen(false); + setSelectedAffectationToDelete(null); + + }; + + // function to detect if there is a project not fully affected ( outside the use effect to mutate the changement of the state) + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.id_project.users.length + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.id_project.users.length - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.id_project.users.length - project.places_occuper, + nombre_personnes: project.id_project.users.length + }; + }); + + return result; + } + + const mutateProjectsAffectaionCheck = (passedData) => { + setListProjectsSemiAffected(filterAndGroupProjects(passedData)) + } + + + + return ( +
+
+ {isOpen && } + {(isOpenCompleteAffectation) && } +

List des Projets attribuer

+
+ +
+ +
+
+ +
+ {(!isLoadingListProjects) ? + (listProjectsAffected && listProjectsAffected.length > 0) ? + + + + + + + + + + + + + + {(listProjectsAffected.map((element, index) => + + {/* */} + + + + + + + + + ))} + +
+ Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions +
+ Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + +
handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
+
+ : +
+ Aucun projet affecter +
+ : +
+ } + { + (listProjectsSemiAffected && listProjectsSemiAffected.length) ?
+ +
+ : + "" + } + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'affectation "${selectedAffectaionToDelete?.id_project.nom}"?`} + /> +
+ + ); +} + + +export default AffectingZoneProject; diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0f8bedf525d6a2136f04143d1d06e9b7637fbcba --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -0,0 +1,57 @@ +import React, { useContext, useState } from 'react'; +import { ReservationContext } from './page'; + +const colors = [ + '#FF5733', '#50cc65', '#3357FF', '#c9ce41', '#FF33A8', + '#24c4b8', '#FF8333', '#8333FF', '#3383FF', '#83FF33', + '#FF3383', '#2ac567', '#FF33F3', '#4cb7be', '#F333FF', + '#cdd927', '#FF33A8', '#80d3cd', '#FF8333', '#8333FF', + '#3383FF', '#92d965', '#FF3383', '#4ebb78', '#FF33F3', +]; + +const getColorForProject = (projectId) => { + const index = projectId % colors.length; + return colors[index]; +}; + +const PlaceUI = ({ id, isTop }) => { + const { allPlaces, bookedPlaces } = useContext(ReservationContext); + const [showTooltip, setShowTooltip] = useState(false); + const place = allPlaces?.find((place) => place.id === id); + const bookedPlace = bookedPlaces?.find((place) => place.id_place === id); + + if (place) { + const backgroundColor = getColorForProject(place.project_id); // Assuming place object has a project_id field + return ( +
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > +

+ {place?.project_name || ""} +

+ {bookedPlace && ( +
+ )} + {showTooltip && bookedPlace && ( +
+

Nom: {bookedPlace.first_name}

+

Prénom: {bookedPlace.last_name}

+

Role: {bookedPlace.role}

+

Presence: {bookedPlace.presence}

+

Crée à: {new Date(bookedPlace.created_at).toLocaleString()}

+
+ )} +
+ ); + } else { + return ( +
+ ); + } +}; + +export default PlaceUI; diff --git a/src/app/(dashboard)/consultation-reservations/TableUI.jsx b/src/app/(dashboard)/consultation-reservations/TableUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ce702a2af8182dceaa21dc700b0ad20d3e6ee9fe --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/TableUI.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import PlaceUI from './PlaceUI'; + +const TableUI = ({id, numero, places}) => { + function groupConsecutive(arr) { + + arr = arr.sort((a, b) => a.id - b.id) + if (arr.length === 0) { + return []; + } + + const grouped = []; + var counter = 0 + while (counter < arr.length) { + if (counter + 1 < arr.length) { + grouped.push([arr[counter], arr[counter + 1]]) + } else { + grouped.push([arr[counter]]) + } + counter += 2; + } + + return grouped; + } + + const processedPlaces = groupConsecutive(places).reverse() + + + if (!processedPlaces || processedPlaces.length === 0) return <> + return ( +
+ {processedPlaces.map((element, index) => { + return
+
+ +
+
+ {(element.length > 1) &&
+ +
} +
+ })} +
+ ) +} + +export default TableUI \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3ea1b5c54124d4f5667af17bb637dd64d32a4a3a --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import TableUI from './TableUI' + +const ZoneUI = ({ id, tables, nom }) => { + return ( +
+

Zone {nom}

+
+ {tables.map((table) => { + return + })} +
+
+ ) +} + +export default ZoneUI \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..253e302e1b4215f1469ff8f844917e1944471119 --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -0,0 +1,290 @@ +'use client' + +import React, { useEffect, useState, useRef } from 'react' +import ZoneUI from './ZoneUI' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' + +export const ReservationContext = React.createContext() + +const Reservation = () => { + const [isLoadingData, setIsLoadingData] = useState(false) + const { toggleNotification } = useNotification() + const [date, setDate] = useState({ day: null, week: null }) + const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) + const [floors, setFloors] = useState([]) + const [projectsData, setProjectsData] = useState([]) + const [currentDateData, setCurrentDateData] = useState(null) + const [datesData, setDatesData] = useState(null) + const [bookedPlaces, setBookedPlaces] = useState([]) + const [ etages, setEtages ] = useState([]) + const [ isLoadingEtages, setIsLoadingEtages ] = useState(true) + const [ selectedEtage, setSelectedEtage ] = useState(null) + const [ zones, setZones ] = useState([]) + const [ isLoadingZones, setIsLoadingZones ] = useState(true) + const [ selectedZone, setSelectedZone ] = useState(null) + + useEffect(() => { + const getPlan = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etage-zone-table-place/') + setIsLoadingSelectsData(false) + if (isSuccess) { + setFloors(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } catch (error) { + console.log(error) + } + } + getPlan() + }, []) + + useEffect(() => { + const getAllEtages = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + setIsLoadingEtages(false) + if (isSuccess) { + setEtages(data) + } else { + setEtages([]) + } + } catch (error) { + setIsLoadingEtages(false) + console.log(error) + } + } + getAllEtages() + }, []) + + useEffect(() => { + if(selectedEtage){ + const concernedEtage = floors.find((element) => element.id == selectedEtage) + setZones(concernedEtage.zones) + } + }, [selectedEtage]) + + console.log(floors) + console.log("selectedEtage", selectedEtage) + console.log("zones", zones) + + + useEffect(() => { + const getSyncData = async () => { + const getUserPlan = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/projects-zones-places/${date.week}/${date.day}`) + if (isSuccess) { + setProjectsData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const getBookedPlaces = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) + if (isSuccess) { + console.log("booked places : :", data) + setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id, created_at: element.created_at, first_name: element.id_user.first_name, last_name: element.id_user.last_name, role: element.id_user.role.name }))) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + setIsLoadingData(true) + await Promise.all([getUserPlan(), getBookedPlaces()]) + setIsLoadingData(false) + } + if (date.week && date.day) getSyncData() + else { + setProjectsData([]) + setBookedPlaces([]) + } + + }, [date.week, date.day]) + + useEffect(() => { + const getCurrentDateData = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/current-date/'); + if (isSuccess) { + setCurrentDateData(data) + } else { + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const YearCalendar = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/dates/'); + if (isSuccess) { + console.log("dates data", data) + setDatesData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const getSyncData = async () => { + await Promise.all([getCurrentDateData(), YearCalendar()]) + } + getSyncData() + }, []) + + const handleChangeDate = (event) => { + if (event.target.value) { + const dateSelected = JSON.parse(event.target.value); + console.log("weekMonthly", dateSelected.weekMonthly); + console.log("day", dateSelected.day); + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) + setSelectedEtage(null) + setSelectedZone(null) + } + else { + setDate({ day: null, week: null }) + setSelectedEtage(null) + } + } + + const handleChangeEtage = (event) =>{ + const etage_id = event.target.value + if(selectedEtage !== etage_id){ + setSelectedEtage(etage_id) + setSelectedZone(null) + setSelectedZone(null) + } + } + + const handleChangeZone = (event) =>{ + const zone_id = event.target.value + if(selectedZone !== zone_id){ + setSelectedZone(zone_id) + } + } + + const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] + const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] + const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + + const dateRef = useRef(null) + useEffect(() => { + if (dateRef.current && currentDateData?.date) { + dateRef.current.value = JSON.stringify(currentDateData) + setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) + } + }, [dateRef.current, currentDateData?.date]) + + + if (isLoadingSelectsData) + return
+ +
+ + const currentMonth = new Date().getMonth() + 1 + // filter dates from now to 2 weeks later + const filteredDatesData = datesData?.filter((element) => { + const date = new Date(element.date) + const month = date.getMonth() + 1 + return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + }) + + return ( +
+
+ {(filteredDatesData && filteredDatesData.length) && } + + +
+ {(!isLoadingData) + ? <> +
+ + { + (selectedEtage) ? + floors.filter((element) => + {if (concernedFloors.includes(element.id) && element.id == selectedEtage) {return element}} + ).map((floor) => { + return
+

Etage {floor.numero}

+ {(selectedZone)? + floor.zones.filter((element) => + {if (concernedZones.includes(element.id) && element.id == selectedZone) {return element}} + ).map((zone, index) => { + return + }) + : + floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + }) + } +
+ }) + : + floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
+

Etage {floor.numero}

+ {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
+ }) + } + {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 + &&
+

Vous êtes en télétravail aujourd'hui.

+
} +
+
+ + :
+ +
+ } +
+ ) +} + +export default Reservation \ No newline at end of file diff --git a/src/app/(dashboard)/etage/AddEtageComponent.jsx b/src/app/(dashboard)/etage/AddEtageComponent.jsx new file mode 100644 index 0000000000000000000000000000000000000000..aaf361f797a0a86cf95d9f66d384a46cf468b1c5 --- /dev/null +++ b/src/app/(dashboard)/etage/AddEtageComponent.jsx @@ -0,0 +1,69 @@ +'use client' +import Loader from '@/components/Loader/Loader' +import React, { useState, useRef } from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' + + + + +const AddEtageComponent = ({ etagesState }) => { + const [numeroEtage, setNumeroEtage] = useState("") + const [isLoading, setIsLoading] = useState(false) + const inputRef = useRef(null) + const { toggleNotification } = useNotification() + + + + const handleNewEtage = (e) => { + setNumeroEtage(e.target.value) + } + + const handleAddNewEtage = async () => { + setIsLoading(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/etages/", { + method: "POST", + body: JSON.stringify({ numero: numeroEtage }) + }) + if (isSuccess) { + setIsLoading(false) + inputRef.current.value = "" + console.log(data) + etagesState((prevEtagesState) => [...prevEtagesState, data]); + toggleNotification({ + type: "success", + message: "L'étage a été créer avec succès.", + visible: true, + }) + } else { + setIsLoading(false) + if (errors.type == "ValidationError") + toggleNotification({ + type: "warning", + message: "Le numéro détage déja existe.", + visible: true, + }) + else { + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la zone.", + visible: true, + }) + } + console.log(errors) + } + + } + + return ( +
+ Nouveau étage: +
+ +
+ +
+ ) +} + +export default (AddEtageComponent) \ No newline at end of file diff --git a/src/app/(dashboard)/etage/page.jsx b/src/app/(dashboard)/etage/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9d69312dc9f14e1af2e1c403ce13ba840f0006b1 --- /dev/null +++ b/src/app/(dashboard)/etage/page.jsx @@ -0,0 +1,126 @@ +"use client" +import React from 'react' +import AddEtageComponent from './AddEtageComponent' +import fetchRequest from '../../lib/fetchRequest' +import { useState, useEffect } from 'react'; +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + + + + +const Etage = () => { + const [etages, setEtages] = useState([]) + const [isLoadingData, setIsLoadingData] = useState(true) + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + const [etageToDelete, setEtageToDelete] = useState(null); + + + // Fetch data from external API + useEffect(() => { + const getAllEtages = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + setIsLoadingData(false) + if (isSuccess) { + setEtages(data) + } else { + setEtages([]) + } + } catch (error) { + setIsLoadingData(false) + console.log(error) + } + } + getAllEtages() + }, []) + const handleDeleteEtage = async (etage) => { + try { + console.log(etage) + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/etages/${etage.id}/`, { method: "DELETE" }) + console.log(isSuccess) + console.log(errors) + console.log(data) + if (isSuccess) { + console.log(etages) + console.log("etage: ", etage) + setEtages((prevEtages) => prevEtages.filter((e) => e.id !== etage.id)); + toggleNotification({ + type: "success", + message: "L'étage a été supprimer avec succès.", + visible: true, + }) + } else { + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la suppression de l'étage.", + visible: true, + }) + } + } catch (error) { + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true, + }) + } + } + + const handleDeleteClick = (etage) => { + setEtageToDelete(etage); + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDeleteEtage(etageToDelete); + setModalOpen(false); + setProjectToDelete(null); + }; + + return ( +
+
+
+

Liste des étages

+
+
+ {(!isLoadingData) ? etages && etages?.length ?
    + {etages?.map((etage, index) => + ( +
  • + Etage numĂ©ro: {etage.numero} +
    { handleDeleteClick(etage) }}> + +
    +
  • + ) + )} +
: +
+ Aucun étage n'a été ajouté +
+ : +
+ +
+ } + +
+
+ +
+
+ setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'étage numéro "${etageToDelete?.numero}"?`} + /> +
+ + ) +} + +export default Etage \ No newline at end of file diff --git a/src/app/(dashboard)/layout.jsx b/src/app/(dashboard)/layout.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a4cd33784062f425514c26b2bc9f53734e97e629 --- /dev/null +++ b/src/app/(dashboard)/layout.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import SideBar from '../ui/SideBar' +import Header from '../ui/Header' + +const layout = ({ children }) => { + return ( + <> +
+
+ +
+ {children} +
+
+ + + ) +} + +export default layout \ No newline at end of file diff --git a/src/app/(dashboard)/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx new file mode 100644 index 0000000000000000000000000000000000000000..32ffc3f7d85b462114f5dbfa8554c60a6f2b4479 --- /dev/null +++ b/src/app/(dashboard)/place/RowPlace.jsx @@ -0,0 +1,227 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react'; +import fetchRequest from '../../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + + + + +const RowPlace = ({ id, numero, table, placesState, tables, filteredPlacesState }) => { + console.log(table) + //states + const [isUpdating, setIsUpdating] = useState(false) + const [numPlace, setNumPlace] = useState(numero) + const [selectedTable, setSelectedTable] = useState(table) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRef = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setNumPlace(numero) + setSelectedTable(table?.id) + selectRef.current.value = table?.id + inputRef.current.value = numero + }, [numero, table]) + + const handleUpdatePlace = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/places/${id}/`, { + method: "PATCH", + body: JSON.stringify({ numero: numPlace, id_table: selectedTable }) + }) + setLoadingStatus(false) + if (isSuccess) { + if (data.message === "NO_CHANGES") { + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } + placesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) + filteredPlacesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "La place a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numéro de la place déjà existe.", + visible: true, + }) + } else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numéro de la place saisie existe déjà.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la place", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La place n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/places/${id}/`, { method: "DELETE" }) + if (isSuccess) { + placesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) + filteredPlacesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La place a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La place n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setNumPlace(numero) + setSelectedTable(table.id) + selectRef.current.value = table.id + inputRef.current.value = numero + } + + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("placeRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + + useEffect(() => { + if (isUpdating && inputRef?.current && selectRef?.current) { + inputRef.current.focus() + selectRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + return ( + + + setNumPlace(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la place "${numero}"?`} + /> + + ) +} + + +export default RowPlace diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..68f65d6352d6bc002d969f4f964e7c24536c3636 --- /dev/null +++ b/src/app/(dashboard)/place/page.jsx @@ -0,0 +1,362 @@ +"use client" +import React from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useState, useEffect, useRef } from 'react'; +import Loader from '@/components/Loader/Loader' +import { isArray } from '../../lib/TypesHelper' +import RowPlace from './RowPlace' +import PlaceIcon from "@/static/image/svg/place.svg" +import { useNotification } from '@/context/NotificationContext' + + + +const Place = () => { + const [places, setPlaces] = useState([]) + const [isLoadingData, setIsLoadingData] = useState(true) + const [tables, setTables] = useState([]) + const [etages, setEtages] = useState([]) + const [zones, setZones] = useState([]) + + const [error, setError] = useState(null) + const [isLoadingAction, setIsLoadingAction] = useState(false) + const [nbrPlacesToCreate, setNbrPlacesToCreate] = useState(null) + const [ selectedTable, setSelectedTable ] = useState(null) + const [ selectedEtage, setSelectedEtage ] = useState(null) + const [ selectedZone, setSelectedZone ] = useState(null) + const [ filteredPlaces, setFilteredPlaces ] = useState([]) + + const { toggleNotification } = useNotification() + + + + + // Fetch data from external API + useEffect(() => { + function extractFilters(data) { + let etagesMap = {}; + let zonesMap = {}; + let tables = []; + + data.forEach(item => { + let etageId = item.id_zone.id_etage.id; + let etageNum = item.id_zone.id_etage.numero; + let zoneId = item.id_zone.id; + let zoneNom = item.id_zone.nom; + let tableId = item.id; + let tableNum = item.numero; + + // Add etage to etagesMap with full details + if (!etagesMap[etageId]) { + etagesMap[etageId] = { id: etageId, numero: etageNum, zones: [] }; + } + + // Add zone to zonesMap with full details + if (!zonesMap[zoneId]) { + zonesMap[zoneId] = { id: zoneId, nom: zoneNom, etage: { id: etageId, numero: etageNum }, tables: [] }; + } + + // Add table to tables array with full details + tables.push({ id: tableId, numero: tableNum, zone: { id: zoneId, nom: zoneNom }, etage: { id: etageId, numero: etageNum } }); + + // Link zones to etages + if (!etagesMap[etageId].zones.some(zone => zone.id === zoneId)) { + etagesMap[etageId].zones.push(zonesMap[zoneId]); + } + + // Link tables to zones + if (!zonesMap[zoneId].tables.some(table => table.id === tableId)) { + zonesMap[zoneId].tables.push({ id: tableId, numero: tableNum }); + } + }); + + // Convert etagesMap and zonesMap to arrays + let etages = Object.values(etagesMap); + let zones = Object.values(zonesMap); + + return { etages, zones, tables }; + } + + const getAllTables = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/tables/', { method: 'GET' }) + if (isSuccess) { + const { etages, zones, tables } = extractFilters(data); + setTables(tables) + setEtages(etages) + setZones(zones) + } else { + setTables([]) + setEtages([]) + setZones([]) + } + } catch (error) { + console.log(error) + } + } + const getAllPlaces = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/places/', { method: 'GET' }) + if (isSuccess) { + setPlaces(data) + setFilteredPlaces(data) + } else { + setPlaces([]) + setFilteredPlaces([]) + } + } catch (error) { + console.log(error) + } + } + getAllPlaces() + getAllTables() + setIsLoadingData(false) + }, []) + + + const handleSearchingPlace = async (e) => { + const numero = e.target.value + try { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/search/place/${numero}`, { method: 'GET' }) + if (isSuccess) { + setPlaces(data) + setFilteredPlaces(data) + } else { + setPlaces([]) + setFilteredPlaces([]) + } + } catch (error) { + console.log(error) + } + } + + + // create component files + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/places/", { + method: "POST", + body: JSON.stringify({ nbrPlaces: nbrPlacesToCreate, id_table: selectedTable.id }) + }) + if (isSuccess) { + setIsLoadingAction(false) + setPlaces(prevPlaceState => [ + ...prevPlaceState, + ...data.map(place => { + const foundTable = tables.find(table => table.id == place.id_table); + return { + ...place, + id_table: { + ...place.id_table, + id_zone: { id: foundTable.zone.id, nom: foundTable.zone.nom, id_etage: { id: foundTable.etage.id, numero: foundTable.etage.numero } }, + id: foundTable.id, + numero: foundTable.numero + } + }; + }) + ]); + setFilteredPlaces(prevPlaceState => [ + ...prevPlaceState, + ...data.map(place => { + const foundTable = tables.find(table => table.id == place.id_table); + return { + ...place, + id_table: { + ...place.id_table, + id_zone: { id: foundTable.zone.id, nom: foundTable.zone.nom, id_etage: { id: foundTable.etage.id, numero: foundTable.etage.numero } }, + id: foundTable.id, + numero: foundTable.numero + } + }; + }) + ]); + setNbrPlacesToCreate(null) + setSelectedTable(null) + setSelectedEtage(null) + setSelectedZone(null) + toggleNotification({ + visible: true, + message: "La table a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + if (errors.type === "ValidationError") { + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numéro de la table saisie déjà existe.", + visible: true, + }) + } + } else { + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la table.", + visible: true, + }) + } + console.log(errors) + } + } + + // Handle the name of zone change + const handleEtageSelection = (event) => { + const etageId = event.target.value + setFilteredPlaces(places.filter(place => place.id_table.id_zone.id_etage.id == etageId)) + if(selectedEtage != etageId){ + const selectedEtage = etages.find(etage => etage.id == etageId) + setSelectedEtage(selectedEtage) + setZones(selectedEtage.zones) + // Extract and format tables from the selected étage's zones + const updatedTables = selectedEtage.zones.flatMap(zone => + zone.tables.map(table => ({ + ...table, + zone: { + id: zone.id, + nom: zone.nom + }, + etage: { + id: selectedEtage.id, + numero: selectedEtage.numero + } + })) + ); + setTables(updatedTables) + setSelectedZone(null) + setSelectedTable(null) + } + } + + const handleZoneSelection = (event) => { + const zoneId = event.target.value + setFilteredPlaces(places.filter(place => place.id_table.id_zone.id == zoneId)) + if(selectedZone != zoneId){ + const refreshTables = selectedEtage.zones.flatMap(zone => + zone.tables.map(table => ({ + ...table, + zone: { + id: zone.id, + nom: zone.nom + }, + etage: { + id: selectedEtage.id, + numero: selectedEtage.numero + } + })) + ); + const concernedZone = zones.find(zone => zone.id == zoneId) + setSelectedZone(concernedZone) + const updatedTables = concernedZone.tables.map(table => { + return refreshTables.find(originalTable => originalTable.id === table.id); + }); + setTables(updatedTables) + setSelectedTable(null) + } + } + const handleTableSelection = (event) => { + const tableId = event.target.value + setFilteredPlaces(places.filter(place => place.id_table.id == tableId)) + if(selectedZone != tableId){ + const selectedTable = tables.find(table => table.id == tableId) + setSelectedTable(selectedTable) + } + } + + const handleNbrPlaceChange = (event) => { + const nbrPlace = event.target.value + setNbrPlacesToCreate(nbrPlace) + } + + + return ( +
+
+
+ {!isLoadingData ? + <> +
+

Ajout d'une place

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+

Liste des Places

+
+
+ +
+ { handleSearchingPlace(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des places..." required /> +
+
+ {isArray(filteredPlaces) && filteredPlaces?.length != 0 && isArray(tables) && tables?.length != 0 ? +
+ + + + + + + {filteredPlaces?.map((element) => { + return + })} +
PlaceTable-Zone-EtageAction
+
+ : +
+

Pas encore des places

+
} + + : +
+ } +
+
+
+ + ) +} + +export default Place \ No newline at end of file diff --git a/src/app/(dashboard)/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0db1822080d008a6a286261b3dfbb57f361cd64f --- /dev/null +++ b/src/app/(dashboard)/planning/PlanningTable.jsx @@ -0,0 +1,78 @@ +import React, {useEffect, useState} from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; + +const PlanningTable = ({ data, typePresences, onTypePresenceChange, selectedProject }) => { + // fetch type presence + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const {toggleNotification} = useNotification() + const [selectedPresence, setSelectedPresence] = useState(''); + + const handleRadioChange = (e) => { + const newPresence = e.target.value; + setSelectedPresence(newPresence); + data.forEach((row, rowIndex) => { + row.days.forEach((day, dayIndex) => { + onTypePresenceChange(rowIndex, dayIndex, newPresence); + }); + }); + }; + + return ( +
+
+ Réglez Tout sur : + {typePresences.map((typePresence) => ( + + ))} +
+ + + + + + + + + + + + + {data.map((row, rowIndex) => ( + + + {row.days.map((day, dayIndex) => ( + + ))} + + ))} + +
Semaine/JourJ1J2J3J4J5
Semaine {row.week} + +
+
+ ); +}; + +export default PlanningTable; diff --git a/src/app/(dashboard)/planning/page.jsx b/src/app/(dashboard)/planning/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f6dc6d33ac762cd5301da62de4c97a9ae1ac4785 --- /dev/null +++ b/src/app/(dashboard)/planning/page.jsx @@ -0,0 +1,251 @@ +'use client'; +import { useEffect, useState } from 'react'; +import Dropdown from "@/app/ui/Dropdown"; +import PlanningTable from "@/app/(dashboard)/planning/PlanningTable"; +import fetchRequest from "@/app/lib/fetchRequest"; +import { useNotification } from "@/context/NotificationContext"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + +const PlanningPage = () => { + const blankData = { + planning_data: [ + { week: 1, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 2, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 3, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 4, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 5, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + ] + } + const [projects, setProjects] = useState([]); + const [selectedProject, setSelectedProject] = useState(''); + const [planningData, setPlanningData] = useState(blankData); + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const { toggleNotification } = useNotification(); + const [typePresences, setTypePresences] = useState([]); + const [isModalOpen, setModalOpen] = useState(false); + + + + const fetchProjects = async () => { + const { isSuccess, errors, data } = await fetchRequest('/projects/'); + if (isSuccess) { + setProjects(data); + setErrors(null); + } else { + console.error("Failed to fetch projects"); + setErrors(errors) + } + }; + useEffect(() => { + + fetchProjects(); + }, []); + + const handleProjectChange = (e) => setSelectedProject(e.target.value); + + const fetchPlanningData = async () => { + if (!selectedProject) return; + + // const { isSuccess, errors, data } = await fetchRequest(`/planning/?project=${selectedProject}`); + const { isSuccess, errors, data } = await fetchRequest(`/plannings/project/${selectedProject}/`); + if (isSuccess) { + // if the project have no data the response is [] in this case we should set the blankData + if (data.length === 0) { + setPlanningData(blankData); + setErrors(null); + return; + } + setPlanningData(data[0]); + setErrors(null); + } else { + console.error("Failed to fetch planning data"); + setPlanningData(blankData) + setErrors(errors); + } + }; + useEffect(() => { + fetchPlanningData(); + }, [selectedProject]); + + const fetchTypePresences = async () => { + const { isSuccess, errors, data } = await fetchRequest('/type-presences/'); + if (isSuccess) { + setTypePresences(data); + setErrors(null); + } else { + console.error("Failed to fetch type presences"); + setErrors(errors) + } + } + useEffect(() => { + fetchTypePresences() + }, []); + + const handleTypePresenceChange = (weekIndex, dayIndex, value) => { + const updatedData = { ...planningData }; + // get the presence type by id (value is a string convert it to int + const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(value)); + console.log(typePresence) + + // the value I want to add should be {"id": 1, "nom": "Travail à Temps Partiel"} and not just the id + updatedData.planning_data[weekIndex].days[dayIndex] = typePresence; + setPlanningData(updatedData); + console.log(value) + console.log(updatedData); + }; + + const handleSave = async () => { + if (!selectedProject) { + toggleNotification({ + visible: true, + message: 'Veuillez sélectionner un projet', + type: 'error' + }); + return; + } + // verify if planing data has an empty value + const emptyValue = planningData.planning_data.some(week => week.days.some(day => day.nom === '')); + if (emptyValue) { + toggleNotification({ + visible: true, + message: 'Veuillez remplir toutes les valeurs de planification', + type: 'error' + }); + return; + } + setLoading(true); + const requestBody = { id_project: selectedProject, planning_data: planningData.planning_data }; + console.log({ id_project: selectedProject, planning_data: planningData.planning_data }) + const { isSuccess, errors } = await fetchRequest(`/plannings/`, { + method: 'POST', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Données de planification enregistrées avec succès', + type: 'success' + }); + fetchPlanningData() + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de lenregistrement des données de planification', + type: 'error' + }); + } + }; + + const handleUpdate = async () => { + setLoading(true); + const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; + const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { + method: 'PUT', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Données de planification mises à jour avec succès', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de la mise à jour des données de planification', + type: 'error' + }); + } + } + + const handleDelete = async () => { + setLoading(true); + // dlete by planning id not project id + const { isSuccess, errors } = await fetchRequest(`/plannings/${planningData.id}/`, { + method: 'DELETE' + }); + setLoading(false); + if (isSuccess) { + setPlanningData(blankData); + toggleNotification({ + visible: true, + message: 'Données de planification supprimées avec succès', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de la suppression des données de planification', + type: 'error' + }); + } + }; + const handleDeleteClick = () => { + setModalOpen(true); + }; + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + return ( +
+

Planning

+ +
+ +
+ {/* crud buttons*/} +
+ + {planningData.id ? + <> + + + : + + } + +
+ setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Etes-vous sûr de vouloir supprimer ces données de planification ?`} + /> +
+ ); +}; + +export default PlanningPage; diff --git a/src/app/(dashboard)/planning/type-presence/EntityForm.jsx b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..408e3a41c2afc4009945d83e6f1d7db4930c3f42 --- /dev/null +++ b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; + +const EntityForm = ({ entity, id, onSaved, onCancel }) => { + const [nom, setNom] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { toggleNotification } = useNotification() + const fetchData = async () => { + const response = await fetchRequest(`/${entity}/${id}/`); + if (response.isSuccess) setNom(response.data.nom); + }; + + useEffect(() => { + if (id) { + fetchData(); + } else { + setNom(''); + } + }, [entity, id]); + + const handleSubmit = async (e) => { + e.preventDefault(); + setIsLoading(true); + + const method = id ? 'PUT' : 'POST'; + const url = id ? `/${entity}/${id}/` : `/${entity}/`; + const response = await fetchRequest(url, { + method, + body: JSON.stringify({ nom }), + }); + + setIsLoading(false); + if (response.isSuccess) { + toggleNotification({ + visible: true, + message: `${nom} a été ${id ? 'modifié' : 'créé'} avec succès`, + type: "success" + }) + setNom(''); + onSaved(); + onCancel(); + } + }; + + return ( +
+
+ + setNom(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + required + /> +
+
+ + +
+
+ ); +}; + +export default EntityForm; diff --git a/src/app/(dashboard)/planning/type-presence/EntityList.jsx b/src/app/(dashboard)/planning/type-presence/EntityList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d2f5d9e4b0f588a3eb15f1e4ce221f9ccf4b3d99 --- /dev/null +++ b/src/app/(dashboard)/planning/type-presence/EntityList.jsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import ConfirmationModal from '@/app/ui/ConfirmationModal'; +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; + +const EntityList = ({ title, items, setState, handleDelete, handleEdit }) => { + const [isModalOpen, setModalOpen] = useState(false); + const [entityToDelete, setEntityToDelete] = useState(null); + + const handleDeleteClick = (entity) => { + setEntityToDelete(entity); + setModalOpen(true); + }; + + const handleConfirmDelete = () => { + handleDelete(title.toLowerCase(), entityToDelete.id, setState, items); + setModalOpen(false); + setEntityToDelete(null); + }; + + return ( +
+ + + + + + + + + {items.map(item => ( + + + + + ))} + +
NomActions
{item.nom} + + +
+ + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Are you sure you want to delete this ${title.toLowerCase()} "${entityToDelete?.nom}"?`} + /> +
+ ); +}; + +export default EntityList; diff --git a/src/app/(dashboard)/planning/type-presence/page.jsx b/src/app/(dashboard)/planning/type-presence/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e3902baf61ab4678b7d1d1f0f060012078cd590d --- /dev/null +++ b/src/app/(dashboard)/planning/type-presence/page.jsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import EntityList from "@/app/(dashboard)/planning/type-presence/EntityList"; +import EntityForm from "@/app/(dashboard)/planning/type-presence/EntityForm"; +import { useNotification } from "@/context/NotificationContext"; + +const ManagePage = () => { + const [typePresences, setTypePresences] = useState([]); + const [editingEntity, setEditingEntity] = useState({ entity: null, id: null }); + const { toggleNotification } = useNotification() + + const fetchData = async () => { + const typePresencesResponse = await fetchRequest('/type-presences/'); + + if (typePresencesResponse.isSuccess) setTypePresences(typePresencesResponse.data); + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleDelete = async (endpoint, id, setState, currentState) => { + const response = await fetchRequest(`/${endpoint}/${id}/`, { method: 'DELETE' }); + if (response.isSuccess) { + setState(currentState.filter(item => item.id !== id)); + toggleNotification({ + visible: true, + message: `${currentState.find(item => item.id === id).nom} a été supprimé avec succès`, + type: "success" + }) + } + await fetchData(); + }; + + return ( +
+

Gérer Les Entités

+
+
+

Type de Presence

+ setEditingEntity({ entity: null, id: null })} + /> + setEditingEntity({ entity: 'type-presences', id })} + /> +
+
+
+ ); +}; + +export default ManagePage; diff --git a/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..861c65b910f8b4c8b386866ac59fb2e7c9c110ef --- /dev/null +++ b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx @@ -0,0 +1,78 @@ +'use client' +import Loader from '@/components/Loader/Loader' +import React, { useRef, useState } from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from "@/context/NotificationContext"; + +const CreatePrivilegeForm = ({ appendPrivilege }) => { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [privilegeName, setPrivilegeName] = useState(""); + const { toggleNotification } = useNotification() + const handlePrivilegeNameChange = (event) => { + setError("") + setPrivilegeName(event.target.value) + } + const inputRef = useRef(null) + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const { data, errors, isSuccess } = await fetchRequest("/privileges/", { + method: "POST", + body: JSON.stringify({ name: privilegeName }) + }) + if (isSuccess) { + setIsLoading(false) + appendPrivilege(data) + inputRef.current.value = "" + setPrivilegeName("") + toggleNotification({ + visible: true, + message: "L'habilitation a été créé avec succès", + type: "success" + }) + } else { + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le nom de l'habilitation existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de l'habilitation", + type: "warning" + }) + } + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + } + return ( +
+

Ajout d'habilitation

+
+ + +
+

{error}

+
+ +
+
+ ) +} + +export default CreatePrivilegeForm \ No newline at end of file diff --git a/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d0527ec777e59d06f3301aadc10b47744500378b --- /dev/null +++ b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx @@ -0,0 +1,174 @@ +'use client' +import React, { useEffect, useRef, useState } from 'react' +import fetchRequest from '../../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '../../ui/ConfirmationModal' +const PrivilegeTableRow = ({ id, name, setPrivileges }) => { + const [isUpdating, setIsUpdating] = useState(false) + const [privilegeName, setPrivilegeName] = useState(name) + const [loadingStatus, setLoadingStatus] = useState(false) + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false) + const showDeletePopup = () => { + setModalOpen(true); + } + useEffect(() => { + setPrivilegeName(name) + inputRef.current.value = name + }, [name]) + const handleUpdatePrivilege = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/privileges/${id}/`, { + method: "PATCH", + body: JSON.stringify({ name: privilegeName }) + }) + setLoadingStatus(false) + if (isSuccess) { + setPrivileges((privileges) => privileges.map((element) => element.id === id ? data.data : element)) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "L'habilitation a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le nom de l'habilitation existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de l'habilitation", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "L'habilitation n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/privileges/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setPrivileges((privileges) => privileges.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "L'habilitation a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "L'habilitation n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + setModalOpen(false) + } + const cancelUpdate = () => { + setIsUpdating(false) + setPrivilegeName(name) + inputRef.current.value = name + } + const inputRef = useRef(null) + const rowRef = useRef(null) + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("privilegeRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + useEffect(() => { + if (isUpdating && inputRef?.current) { + inputRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + return ( + <> + + + setPrivilegeName(event.target.value)} defaultValue={name} type='text' className='disabled:bg-white w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer l'habilitation ?`} + /> + + ) + +} + +export default PrivilegeTableRow \ No newline at end of file diff --git a/src/app/(dashboard)/privilege/page.jsx b/src/app/(dashboard)/privilege/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f10039bf8b06739cf424d8b4c759122b37c03223 --- /dev/null +++ b/src/app/(dashboard)/privilege/page.jsx @@ -0,0 +1,51 @@ +'use client' +import React, { useEffect, useState } from 'react' +import CreatePrivilegeForm from './CreatePrivilegeForm' +import fetchRequest from '../../lib/fetchRequest' +import { isArray } from '../../lib/TypesHelper' +import PrivilegeTableRows from './PrivilegeTableRow' +import Loader from '@/components/Loader/Loader' +const Privilege = () => { + const [privileges, setPrivileges] = useState([]) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + setIsLoading(false) + if (isSuccess) { + setPrivileges(data) + } else { + console.log(errors) + } + } + getPrivileges() + }, []) + const appendPrivilege = (privilege) => { + setPrivileges((data) => [privilege, ...data]) + } + return ( +
+ +

Liste des habilitations

+ {isLoading &&
} + {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) + ?
+

Pas encore des habilitations

+
+ :
+ + + + + + {privileges.map((element) => { + return + })} +
HabilitationAction
+
+ }} +
+ ) +} + +export default Privilege \ No newline at end of file diff --git a/src/app/(dashboard)/projects/ProjectForm.jsx b/src/app/(dashboard)/projects/ProjectForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bb531d52e9639b8047f24d18afc63b4444fb5b18 --- /dev/null +++ b/src/app/(dashboard)/projects/ProjectForm.jsx @@ -0,0 +1,198 @@ +import { useState, useEffect } from 'react'; +import fetchRequest from '@/app/lib/fetchRequest'; + +const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingProject }) => { + const [projectName, setProjectName] = useState(''); + const [clientName, setClientName] = useState(''); + const [dateDebut, setDateDebut] = useState(''); + const [dateFin, setDateFin] = useState(''); + const [userIds, setUserIds] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [userSuggestions, setUserSuggestions] = useState([]); + const [errors, setErrors] = useState({}); + + useEffect(() => { + if (editingProject) { + setProjectName(editingProject.nom); + setClientName(editingProject.nomClient); + setDateDebut(editingProject.dateDebut); + setDateFin(editingProject.dateFin); + setUserIds(editingProject.users || []); + } else { + setProjectName(''); + setClientName(''); + setDateDebut(''); + setDateFin(''); + setUserIds([]); + } + }, [editingProject]); + + useEffect(() => { + if (searchQuery.length > 1) { + const fetchUsers = async () => { + const { isSuccess, data } = await fetchRequest(`/search-users/?q=${searchQuery}`); + if (isSuccess) { + setUserSuggestions(data); + } + }; + fetchUsers(); + } else { + setUserSuggestions([]); + } + }, [searchQuery]); + + const handleUserSelect = (user) => { + setUserIds([...userIds, user]); + setSearchQuery(''); + setUserSuggestions([]); + }; + + const handleRemoveUserId = (index) => { + setUserIds(userIds.filter((_, i) => i !== index)); + }; + + const validateForm = () => { + const newErrors = {}; + if (!projectName) newErrors.projectName = "Project name is required"; + if (!clientName) newErrors.clientName = "Client name is required"; + if (!dateDebut) newErrors.dateDebut = "Start date is required"; + if (!userIds.length) newErrors.userIds = "At least one user is required"; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!validateForm()) return; + const project = { + nom: projectName, + nomClient: clientName, + dateDebut, + dateFin, + user_ids: userIds.map(user => user.id), + }; + console.log(project) + console.log(project.users) + if (editingProject) { + onEditProject(editingProject.id, project); + } else { + onAddProject(project); + } + }; + + const isUserChosen = (user) => { + return userIds.some(chosenUser => chosenUser.id === user.id); + }; + + return ( +
+
+ + setProjectName(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required + /> + {errors.projectName &&

{errors.projectName}

} +
+
+ + setClientName(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required + /> + {errors.clientName &&

{errors.clientName}

} +
+
+
+ + setDateDebut(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required + /> + {errors.dateDebut &&

{errors.dateDebut}

} +
+
+ + setDateFin(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + /> + {errors.dateFin &&

{errors.dateFin}

} +
+
+
+ + setSearchQuery(e.target.value)} + placeholder="Tapez pour rechercher des utilisateurs..." + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + /> + {userSuggestions.length > 0 && ( +
    + {userSuggestions.map(user => ( +
  • !isUserChosen(user) && handleUserSelect(user)} + className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-chicago-100 cursor-not-allowed' : 'hover:bg-sushi-200 cursor-pointer'}`} + > + {/*add tick is chosen*/} + {isUserChosen(user) && 'âś” '} + {user.first_name} {user.last_name} ({user.role.name}) {user.email} +
  • + ))} +
+ )} +
+ {userIds.map((user, index) => ( +
+ + +
+ ))} +
+ {errors.userIds &&

{errors.userIds}

} +
+ + {/* cancel*/} + {editingProject && ( + + )} +
+ ); +}; + +export default ProjectForm; diff --git a/src/app/(dashboard)/projects/ProjectList.jsx b/src/app/(dashboard)/projects/ProjectList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..67721cf53b5a616a709dd45c7877432913dcc00a --- /dev/null +++ b/src/app/(dashboard)/projects/ProjectList.jsx @@ -0,0 +1,69 @@ +import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import React, { useState } from "react"; +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; + + + +const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { + const [isModalOpen, setModalOpen] = useState(false); + const [projectToDelete, setProjectToDelete] = useState(null); + const handleDeleteClick = (project) => { + setProjectToDelete(project); + setModalOpen(true); + }; + + const handleConfirmDelete = () => { + onDelete(projectToDelete); + setModalOpen(false); + setProjectToDelete(null); + }; + return ( + <> + + + + + + + + + + + {projects + .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + .map((project, index) => ( + + + + + + + ))} + +
ProjetMembres de léquipeNom du clientActions
{project.nom}{project.users.length}{project.nomClient} + + +
+ {/* Confirmation Modal */} + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Are you sure you want to delete the project "${projectToDelete?.nom}"?`} + /> + + ); +}; + +export default ProjectList; diff --git a/src/app/(dashboard)/projects/page.jsx b/src/app/(dashboard)/projects/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..230000ed459f7ca8745c677ef6103a005388cdc3 --- /dev/null +++ b/src/app/(dashboard)/projects/page.jsx @@ -0,0 +1,229 @@ +'use client'; + +import SideBar from "@/app/ui/SideBar"; +import { useEffect, useState } from 'react'; +import ProjectForm from "@/app/(dashboard)/projects/ProjectForm"; +import ProjectList from "@/app/(dashboard)/projects/ProjectList"; +import fetchRequest from "@/app/lib/fetchRequest"; +import {PAGINATION_SIZE} from "@/app/lib/constants"; +import Pagination from "@/app/ui/Pagination"; +import { useNotification } from "@/context/NotificationContext"; + +const Projects = () => { + const [pageUrl, setPageUrl] = useState('/projects/pagination/'); + const [projects, setProjects] = useState([]); + const [editingProject, setEditingProject] = useState(null); + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const [isFormOpen, setIsFormOpen] = useState(false); // State for accordion + const [searchQuery, setSearchQuery] = useState(''); // State for search query + const [pagination, setPagination] = useState({}); // State for pagination + const { toggleNotification } = useNotification(); + + const fetchProjects = async (url = pageUrl) => { + const { isSuccess, errors, data } = await fetchRequest(url); + if (isSuccess) { + setProjects(data.results); + setPagination({ + currentPage: data.currentPage, + pagesNumber: Math.ceil(data.count / PAGINATION_SIZE), + next: data.next, + previous: data.previous, + count: data.count + }); + setErrors(null); + } else { + console.error("Failed to fetch projects"); + setErrors(errors); + } + }; + + const handleSearch = async (query) => { + setLoading(true); + const { isSuccess, errors, data } = await fetchRequest(`/search-projects/?q=${query}`); + if (isSuccess) { + setProjects(data.results); + setPagination({ + currentPage: data.currentPage, + pagesNumber: Math.ceil(data.count / PAGINATION_SIZE), + next: data.next, + previous: data.previous, + count: data.count + }); + setErrors(null); + } else { + console.error("Failed to search projects"); + setErrors(errors); + } + setLoading(false); + }; + + useEffect(() => { + fetchProjects(); + }, [pageUrl]); + + const handleAddProject = async (project) => { + setLoading(true); + const { isSuccess, errors, data } = await fetchRequest('/projects/', { + method: 'POST', + body: JSON.stringify(project), + }); + + if (isSuccess) { + toggleNotification({ + visible: true, + message: `${project.nom} a été ajouté avec succès`, + type: "success" + }); + setProjects([...projects, data]); + setEditingProject(null); + setErrors(null); + // setIsFormOpen(false); // Close the form on success + } else { + console.error("Failed to add project"); + setErrors(errors); + } + setLoading(false); + setIsFormOpen(false) + }; + + const handleEditProject = async (id, updatedProject) => { + setLoading(true); + const { isSuccess, errors, data } = await fetchRequest(`/projects/${id}/`, { + method: 'PUT', + body: JSON.stringify(updatedProject), + }); + + if (isSuccess) { + toggleNotification({ + visible: true, + message: `${updatedProject.nom} a été modifié avec succès`, + type: "success" + }); + const updatedProjects = projects.map((project) => + project.id === id ? data : project + ); + setProjects(updatedProjects); + setEditingProject(null); + setErrors(null); + // setIsFormOpen(false); // Close the form on success + } else { + console.error("Failed to edit project"); + setErrors(errors); + } + setLoading(false); + }; + + const handleEditClick = (project) => { + setEditingProject(project); + setIsFormOpen(true); // Open the form for editing + }; + + const handleDeleteProject = async (project) => { + setLoading(true); + const { isSuccess, errors } = await fetchRequest(`/projects/${project.id}/`, { + method: 'DELETE', + }); + + if (isSuccess) { + toggleNotification({ + visible: true, + message: `${project.nom} a été supprimé avec succès`, + type: "success" + }); + await fetchProjects(); + setEditingProject(null); + setErrors(null); + } else { + console.error("Failed to delete project"); + setErrors(errors); + } + setLoading(false); + }; + + const toggleForm = () => { + setIsFormOpen(!isFormOpen); // Toggle the form visibility + }; + + const handleSearchInputChange = (e) => { + const query = e.target.value; + setSearchQuery(query); + handleSearch(query); // Trigger search on input change + }; + + const handlePageChange = (url) => { + setPageUrl(url); + }; + + return ( +
+
+
+
+

Projets + {loading ? ... : null} +

+ {/* Accordion Toggle Button */} + +
+ +
+ +
+ + {/* Accordion Content */} + {isFormOpen && ( + + )} + + {/* Errors from request */} + {errors && errors.detail && Object.keys(errors.detail).map((key) => ( +
+
+ + + + Something went wrong +
+

+ {errors.detail[key]} +

+
+ ))} + + + + + +
+
+
+ ); +}; + +export default Projects; diff --git a/src/app/(dashboard)/reporting/BubbleStatistic.jsx b/src/app/(dashboard)/reporting/BubbleStatistic.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a3d82a4170b120a41d02a142c15a0ae90842637a --- /dev/null +++ b/src/app/(dashboard)/reporting/BubbleStatistic.jsx @@ -0,0 +1,91 @@ +"use client" +import React, { memo, useMemo, useState } from 'react' +import { Bubble } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend +} from 'chart.js'; +import { generateColors } from '@/app/lib/colorsGenerator'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend +); + + +const BubbleStatistic = React.memo(function BubbleStatistic({ axisX, data, title }) { + + const colors = useMemo(() => data ? generateColors(data.length) : [], [data]) + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'category', + labels: axisX, + }, + y: { + type: 'linear', + min: 0, + max: 125, + ticks: { + stepSize: 25, + callback: function (value) { + return value.toFixed(2).toString() + '%'; + } + }, + title: { + display: true, + text: 'Présence', + color: '#333', + font: { + size: 14, + weight: 'bold' + } + } + } + }, + plugins: { + tooltip: { + callbacks: { + label: function (context) { + const data = context.dataset.data[context.dataIndex]; + const label = context.dataset.label || ''; + return `${label}: (${data.x}, ${data.y.toFixed(2)}%)`; + } + } + } + } + }; + + const chartData = useMemo(() => { + if (!data) return { + datasets: [] + } + return { + datasets: data.map((element, index) => ({ + ...element, + backgroundColor: colors[index].backgroundColor, + borderColor: colors[index].borderColor, + borderWidth: 1 + })) + }; + }, [data, colors]) + return ( +
+

{title}

+
+ +
+
+ ); +}) + +export default BubbleStatistic; \ No newline at end of file diff --git a/src/app/(dashboard)/reporting/page.jsx b/src/app/(dashboard)/reporting/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..47058e4bce01ba403b2a2878ac147e39372cbd40 --- /dev/null +++ b/src/app/(dashboard)/reporting/page.jsx @@ -0,0 +1,203 @@ +"use client" + + +import React, { useState, useEffect, useRef, useMemo } from 'react' +import BubbleStatistic from './BubbleStatistic'; +import fetchRequest from '@/app/lib/fetchRequest'; +import Loader from '@/components/Loader/Loader'; +import { useNotification } from '@/context/NotificationContext'; +import { extractDate, getDateRange, subtractDays } from '@/app/lib/DateHelper'; +import UserIcon from "@/static/image/svg/user.svg" +import ProjectIcon from "@/static/image/svg/project.svg" +import StatisticsIcon from "@/static/image/svg/statistics.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" + + +const Reporting = () => { + const [chartDataZone, setChartDataZone] = useState(null) + const [chartDataProject, setChartDataProject] = useState(null) + const [isLoadingZone, setIsLoadingZone] = useState(false) + const [isLoadingProject, setIsLoadingProject] = useState(false) + const [ countUsers, setCountUsers ] = useState(0) + const [ countProjects, setCountProjects ] = useState(0) + const [ countPlaces, setCountPlaces ] = useState(0) + const { toggleNotification } = useNotification() + const [dates, setDates] = useState({ fromDate: extractDate(subtractDays(new Date(), 4)), toDate: extractDate(new Date()) }) + + useEffect(() => { + const getStats= async () => { + const { data, errors, isSuccess } = await fetchRequest(`/statistics/`, { + method: "GET", + }) + if (isSuccess) { + setCountUsers(data.user_count) + setCountProjects(data.project_count) + setCountPlaces(data.place_count) + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getStats() + }, []) + console.log(countPlaces) + console.log(countProjects) + console.log(countUsers) + + useEffect(() => { + const getZonesPresenceStatistic = async () => { + setIsLoadingZone(true) + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/zone-presence/`, { + method: "POST", + body: JSON.stringify({ + from_date: dates.fromDate, + to_date: dates.toDate + }) + }) + setIsLoadingZone(false) + if (isSuccess) { + setChartDataZone(data); + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getZonesPresenceStatistic() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dates.fromDate, dates.toDate]) + + + useEffect(() => { + const getProjectsPresenceStatistic = async () => { + setIsLoadingProject(true) + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/project-presence/`, { + method: "POST", + body: JSON.stringify({ + from_date: dates.fromDate, + to_date: dates.toDate + }) + }) + setIsLoadingProject(false) + if (isSuccess) { + console.log(data); + setChartDataProject(data); + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getProjectsPresenceStatistic() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dates.fromDate, dates.toDate]) + const handleDateChange = (event) => { + const name = event.target.name + const value = event.target.value + if (value) { + setDates({ ...dates, [name]: value }) + return; + } + else if (!value && name === "fromDate") setDates({ ...dates, "fromDate": extractDate(subtractDays(dates.toDate, 5)) }) + else if (!value && name === "toDate") setDates({ ...dates, "toDate": extractDate(new Date()) }) + } + const axisX = useMemo(() => getDateRange(dates.fromDate, dates.toDate), [dates.fromDate, dates.toDate]) + const processedProjectData = useMemo(() => { + return chartDataProject?.map((project, index) => ( + { + label: project.label, + data: [...project.data.map((element) => ( + { + x: element.date.split("-").reverse().join("-"), + y: element.pourcentage, r: (index * 2) + 6 + } + )), ...axisX.filter((element) => !project.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + x: element, + y: 0, + r: (index * 2) + 6 + }))] + })) + }, [chartDataProject, axisX]) + + const processedZoneData = useMemo(() => { + return chartDataZone?.map((zone, index) => ( + { + label: "Zone " + zone.label, + data: [...zone.data.map((element) => ( + { + x: element.date.split("-").reverse().join("-"), + y: element.pourcentage, r: (index * 2) + 6 + } + )), ...axisX.filter((element) => !zone.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + x: element, + y: 0, + r: (index * 2) + 6 + }))] + })) + }, [chartDataZone, axisX]) + return ( +
+
+
+

Collaborateurs

+
+ {countUsers} +
+ + +
+
+
+
+

Projets

+
+ {countProjects} +
+ + +
+
+
+
+

Places

+
+ {countPlaces} +
+ + +
+
+
+
+
+ + +
+
+ + + {/* */} +
+
+
+ {(isLoadingZone || isLoadingProject) ?
+ +
:
+ + +
} +
+ ) +} + +export default Reporting \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..43d2f8f76985f016934e10178a36cc79c3f35f89 --- /dev/null +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -0,0 +1,125 @@ +"use client" +import React, { useContext, useState } from 'react' +import { ReservationContext } from './page' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '@/app/ui/ConfirmationModal' +const PlaceUI = ({ id }) => { + const { allPlaces, selectedDate, bookedPlaces, setBookedPlaces, authenticatedUserData, hasPlace } = useContext(ReservationContext) + const { toggleNotification } = useNotification() + const [isOpenBooking, setIsOpenBooking] = useState(false) + const [isOpenCanceling, setIsOpenCanceling] = useState(false) + + const place = allPlaces?.find((place) => place.id === id) + const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) + const handleBooking = (event) => { + event.stopPropagation() + if (hasPlace && hasPlace.presence) return; + if (hasPlace) { + toggleNotification({ + visible: true, + message: "Veuillez annuler votre réservation pour réserver une nouvelle place .", + type: "warning" + }) + } + else setIsOpenBooking(true) + } + const handleBookingConfirmation = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/reservations/', { + method: "POST", body: JSON.stringify( + { + "presence": false, + "date": selectedDate, + "id_place": id + } + ) + }); + if (isSuccess) { + console.log(data); + setBookedPlaces([...bookedPlaces, data]) + toggleNotification({ + visible: true, + message: "La réservation a été enregistrer avec succés", + type: "success" + }) + setIsOpenBooking(false) + } else { + if (errors.type === "ValidationError" && errors.detail?.non_field_errors && errors.detail?.non_field_errors[0]?.indexOf("date, id_place must make a unique set") !== -1) { + toggleNotification({ + visible: true, + message: "La place a été déjà réservée par votre collègue", + type: "warning" + }) + } + else toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + const handleCancelingConfirmation = async (idReservation) => { + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${idReservation}`, { + method: "DELETE" + }); + if (isSuccess) { + console.log(data); + setBookedPlaces(bookedPlaces.filter((element) => element.id !== idReservation)) + toggleNotification({ + visible: true, + message: "La réservation a été annuler avec succés", + type: "success" + }) + setIsOpenCanceling(false) + } else { + if (status === 404) + toggleNotification({ + visible: true, + message: "La réservation n'existe pas", + type: "warning" + }) + else toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + + const handleCanceling = (event) => { + event.stopPropagation() + setIsOpenCanceling(true) + } + const closeCancelingPopup = (event) => { + event.stopPropagation() + setIsOpenCanceling(false) + } + const closeConfirmationPopup = (event) => { + event.stopPropagation() + setIsOpenBooking(false) + } + + + if (authenticatedUserData) + if (place) + if (bookedPlace) + if (bookedPlace.id_user === authenticatedUserData.sessionData?.user_id) + return
{ }} className={`${bookedPlace.presence ? "bg-green-500/80" : "bg-amber-500/80 cursor-pointer"} absolute items-center flex justify-center h-full px-[2px] w-full text-sm`}> +

{place.project_name || ""}

+ handleCancelingConfirmation(bookedPlace.id)} onClose={closeCancelingPopup} /> +
+ else return
+

{place.project_name || ""}

+
+ else return
+

{place.project_name || ""}

+ +
+ else return
+ else return <> +} + + +export default PlaceUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/PresenceButton.jsx b/src/app/(dashboard)/reservation/PresenceButton.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ae586779712c63922b807b1025dd52d959a1edba --- /dev/null +++ b/src/app/(dashboard)/reservation/PresenceButton.jsx @@ -0,0 +1,105 @@ +"use client" +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import React, { useContext, useEffect, useState } from 'react' +import { ReservationContext } from './page' + +const PresenceButton = ({ processUserLocation, geolocationError, isInside }) => { + const { toggleNotification } = useNotification() + const [isLoading, setIsLoading] = useState(false) + const { setBookedPlaces, hasPlace } = useContext(ReservationContext) + const handlePresenceSave = async () => { + if (hasPlace) { + setIsLoading(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${hasPlace.id}/`, { + method: "PATCH", + body: JSON.stringify({ presence: true }) + }) + setIsLoading(false) + if (isSuccess) { + toggleNotification({ + visible: true, + message: "Votre présence a été enregistrée avec succès.", + type: "success" + }) + console.log(data) + setBookedPlaces((elements) => elements.map((element) => element.id === data.data?.id ? data.data : element)) + } + else { + console.log(errors) + if (status === 404) + toggleNotification({ + visible: true, + message: "Aucune réservation existe dans cette date.", + type: "warning" + }) + else + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + } + useEffect(() => { + if ("User denied the request for Geolocation." === geolocationError) { + toggleNotification({ + visible: true, + message: "Vous devez activer la géolocalisation pour enregistrer la présence", + type: "warning" + }) + } + else if (geolocationError === "The request to get user location timed out.") { + toggleNotification({ + visible: true, + message: "Vous devez activer la géolocalisation pour enregistrer la présence", + type: "warning" + }) + } + else if (geolocationError === "Location information is unavailable.") { + toggleNotification({ + visible: true, + message: "Erreur de géolocation", + type: "error" + }) + } + }, [geolocationError]) + + + if ("User denied the request for Geolocation." === geolocationError) { + return
+

Autorisez la géolocalisation pour enregistrer votre présence

+ +
+ } + else if (geolocationError === "The request to get user location timed out.") { + return
+

Autorisez la géolocalisation pour enregistrer votre présence

+ +
+ } + else if (geolocationError === "Location information is unavailable.") { + return + } + else if (isInside) + return + else return
+

Vous n'êtes pas géolocalisé à l'intérieur de Teamwill.

+ +
+} + + +export default PresenceButton \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bf4e5e6a0d402f0cecc996a8b87ba7e30b0ac8d8 --- /dev/null +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import PlaceUI from './PlaceUI'; + +const TableUI = ({ places }) => { + function groupConsecutive(arr) { + + arr = arr.sort((a, b) => a.id - b.id) + if (arr.length === 0) { + return []; + } + + const grouped = []; + var counter = 0 + while (counter < arr.length) { + if (counter + 1 < arr.length) { + grouped.push([arr[counter], arr[counter + 1]]) + } + else { + grouped.push([arr[counter]]) + } + counter += 2; + } + + return grouped; + } + const processedPlaces = groupConsecutive(places).reverse() + + + if (!processedPlaces || processedPlaces.length === 0) return <> + return ( +
+ {processedPlaces.map((element, index) => { + return
+
+ +
+
+ {(element.length > 1) &&
+ +
} +
+ })} +
+ ) +} + +export default TableUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx new file mode 100644 index 0000000000000000000000000000000000000000..42538fde425c979c9be988919bbf6c3774c10398 --- /dev/null +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import TableUI from './TableUI' + +const ZoneUI = ({ id, tables, nom }) => { + return ( +
+

Zone {nom}

+
+ {tables.map((table) => { + return + })} +
+
+ ) +} + +export default ZoneUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e77c657132fece448ba7ce94ecd204fde17fb2ce --- /dev/null +++ b/src/app/(dashboard)/reservation/page.jsx @@ -0,0 +1,332 @@ +'use client' + +import React, { useEffect, useState, useMemo, useRef } from 'react' +import ZoneUI from './ZoneUI' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import PresenceButton from './PresenceButton' +import Cookies from 'js-cookie'; +import { decrypt } from '@/app/lib/session'; + +export const ReservationContext = React.createContext() + +const Reservation = () => { + const [isLoadingData, setIsLoadingData] = useState(true) + const { toggleNotification } = useNotification() + const [date, setDate] = useState({ day: null, week: null }) + const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) + const [floors, setFloors] = useState([]) + const [projectsData, setProjectsData] = useState([]) + const [currentDateData, setCurrentDateData] = useState(null) + const [datesData, setDatesData] = useState(null) + const [bookedPlaces, setBookedPlaces] = useState([]) + const [isInside, setIsInside] = useState(null) + const [geolocationError, setGeolocationError] = useState(null) + + const [authenticatedUserData, setAuthenticatedUserData] = useState(null) + + const cookie = Cookies.get("session") + const getUserData = async () => { + try { + if (cookie) { + const data = await decrypt(cookie) + setAuthenticatedUserData(data) + } + } catch (error) { + console.log(error) + } + } + useMemo(getUserData, [cookie]) + + const processUserLocation = () => { + const companyLatitude = 36.8402141; // Example: Company's latitude + const companyLongitude = 10.2432573; // Example: Company's longitude + const allowedRadius = 300; // Example: Radius in meters + + + function getDistanceFromLatLonInMeters(lat1, lon1, lat2, lon2) { + const R = 6371000; // Radius of the Earth in meters + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; // Distance in meters + return distance; + } + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + function (position) { + const userLatitude = position.coords.latitude; + const userLongitude = position.coords.longitude; + + const distance = getDistanceFromLatLonInMeters( + userLatitude, + userLongitude, + companyLatitude, + companyLongitude + ); + + if (distance <= allowedRadius) { + setIsInside(true) + } else { + setIsInside(false) + } + setGeolocationError(null) + }, + function (error) { + switch (error.code) { + case error.PERMISSION_DENIED: + setGeolocationError("User denied the request for Geolocation."); + break; + case error.POSITION_UNAVAILABLE: + setGeolocationError("Location information is unavailable."); + break; + case error.TIMEOUT: + setGeolocationError("The request to get user location timed out."); + break; + case error.UNKNOWN_ERROR: + setGeolocationError("An unknown error occurred."); + break; + } + } + ); + } else { + toggleNotification({ + message: "La géolocalisation n'est pas supporté par votre navigateur.", + type: "warning", + visible: true + }) + } + } + + + useEffect(() => { + const handlePermissionChange = (permissionStatus) => { + if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') { + processUserLocation(); + } else { + setGeolocationError("User denied the request for Geolocation."); + } + }; + + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + handlePermissionChange(permissionStatus); + + permissionStatus.onchange = () => { + handlePermissionChange(permissionStatus); + }; + }).catch((error) => { + console.error('Failed to query geolocation permission:', error); + }); + return () => { + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + permissionStatus.onchange = null; + }); + }; + }, []) + + useEffect(() => { + const getPlan = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etage-zone-table-place/') + setIsLoadingSelectsData(false) + if (isSuccess) { + setFloors(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } catch (error) { + console.log(error) + } + } + getPlan() + }, []) + + + + useEffect(() => { + const getSyncData = async () => { + const getUserPlan = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/user-projects-zones-places/${date.week}/${date.day}`) + if (isSuccess) { + setProjectsData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const getBookedPlaces = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) + if (isSuccess) { + console.log("booked places : :", data) + setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id }))) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + setIsLoadingData(true) + await Promise.all([getUserPlan(), getBookedPlaces()]) + setIsLoadingData(false) + } + if (date.week && date.day) getSyncData() + else { + setProjectsData([]) + setBookedPlaces([]) + } + + }, [date.week, date.day]) + + useEffect(() => { + const getCurrentDateData = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/current-date/'); + if (isSuccess) { + setCurrentDateData(data) + } else { + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const YearCalendar = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/dates/'); + if (isSuccess) { + console.log("dates data", data) + setDatesData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const getSyncData = async () => { + await Promise.all([getCurrentDateData(), YearCalendar()]) + } + getSyncData() + }, []) + + const handleChangeDate = (event) => { + if (event.target.value) { + const dateSelected = JSON.parse(event.target.value); + + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) + } + else { + setDate({ day: null, week: null }) + } + } + + const currentDate = new Date().toJSON()?.split("T")[0] || null + const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] + const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] + const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + const hasPlace = bookedPlaces?.find((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + + const dateRef = useRef(null) + useEffect(() => { + if (dateRef.current && currentDateData?.date) { + dateRef.current.value = JSON.stringify(currentDateData) + setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) + } + }, [dateRef.current, currentDateData?.date]) + if (isLoadingSelectsData) + return
+ +
+ + const currentMonth = new Date().getMonth() + 1 + // filter dates from now to 2 weeks later + const filteredDatesData = datesData?.filter((element) => { + const date = new Date(element.date) + const month = date.getMonth() + 1 + return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + }) + return ( +
+
+ {(filteredDatesData && filteredDatesData.length) && } +
+
+
+
+

Autres Projets

+
+
+
+

Disponible

+
+
+
+

Réservé par vous

+
+
+
+

Réservé par un collègue

+
+
+
+

Présent

+
+
+ {(!isLoadingData) + ? + + <> + {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0 && hasPlace && !hasPlace.presence && currentDate && currentDate === date?.date) &&
+ +
} +
+ {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
+

Etage {floor.numero}

+ {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
+ })} + {!isLoadingData && floors.filter((element) => concernedFloors.includes(element.id)).length === 0 + &&
+

Vous êtes en télétravail aujourd'hui.

+
} +
+ +
+ :
+ +
+ } +
+ ) +} + +export default Reservation \ No newline at end of file diff --git a/src/app/(dashboard)/role/CreateRoleForm.jsx b/src/app/(dashboard)/role/CreateRoleForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3ba4a859863b9c1f5d662cd800fb10c3c48d5b10 --- /dev/null +++ b/src/app/(dashboard)/role/CreateRoleForm.jsx @@ -0,0 +1,144 @@ +import Loader from '@/components/Loader/Loader' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +const CreateRoleForm = ({ appendRole, setIsOpen }) => { + const [isLoading, setIsLoading] = useState(false) + const [roleName, setRoleName] = useState("") + const [privileges, setPrivileges] = useState(null) + const [selectedPrivileges, setSelectedPrivileges] = useState([]) + const { toggleNotification } = useNotification() + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + if (isSuccess) { + setPrivileges(data) + } else { + setPrivileges([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getPrivileges() + }, []) + const handleRoleNameChange = (event) => { + setRoleName(event.target.value) + } + const inputRef = useRef(null) + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const { data, errors, isSuccess, status } = await fetchRequest("/roles/", { + method: "POST", + body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.map((element) => element.id) }) + }) + if (isSuccess) { + setIsLoading(false) + inputRef.current.value = "" + setSelectedPrivileges([]) + setRoleName("") + appendRole(data) + toggleNotification({ + visible: true, + message: "Le rôle a été créé avec succès", + type: "success" + }) + setIsOpen(false) + } else { + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + visible: true, + message: "Le rôle existe déjà", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle", + type: "warning" + }) + setIsOpen(false) + } + } + else if (status === 409) { + toggleNotification({ + visible: true, + message: "Roles created with 2 or more same privileges", + type: "error" + }) + setIsOpen(false) + } else if (errors.detail === "Privilege matching query does not exist.") { + toggleNotification({ + visible: true, + message: "Des privilèges que vous avez utilisés ont été supprimés.", + type: "warning" + }) + setIsOpen(false) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setIsOpen(false) + } + console.log(errors) + } + } + const handlePrivilegeClick = (privilege) => { + if (selectedPrivileges.find((element) => element.id === privilege.id)) { + setSelectedPrivileges(selectedPrivileges.filter((element) => element.id !== privilege.id)) + } else { + setSelectedPrivileges([...selectedPrivileges, privilege]) + } + } + const selectAll = () => { + if (privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id))) + setSelectedPrivileges([]) + else setSelectedPrivileges(privileges) + } + var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) + return ( +
+
+ setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + {(privileges) ?
+

Ajout de RĂ´le

+
+ + +
+
+
+ +
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
+
+
+ {privileges.length !== 0 ? privileges?.map((privilege) => { + const isSelected = selectedPrivileges.find((element) => element.id === privilege.id) !== undefined + return
handlePrivilegeClick(privilege)} key={privilege.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}>{privilege.name}
+ }) :
+

Pas encore des habilitations

+
} +
+
+
+ +
+
:
} +
+
+ ) +} + +export default CreateRoleForm \ No newline at end of file diff --git a/src/app/(dashboard)/role/RoleTableRows.jsx b/src/app/(dashboard)/role/RoleTableRows.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ccdcad936b6b9f077fc3ac86eb584d85d919d691 --- /dev/null +++ b/src/app/(dashboard)/role/RoleTableRows.jsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '@/app/ui/ConfirmationModal' + +const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + + const showDeletePopup = () => { + setModalOpen(true); + } + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/roles/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setRoles((roles) => roles.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "Le rôle a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "Le rôle n'a pas été trouvé", + type: "warning" + }) + } else if (errors.detail?.indexOf("Cannot delete some instances of model 'Role'") !== -1) { + toggleNotification({ + visible: true, + message: "Impossible de supprimer ce rôle car il est attribué à des utilisateurs", + type: "warning" + }) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + setModalOpen(false) + console.log(errors) + } + + return ( + <> + + +

{name}

+ + +
+ {privileges?.map((element, index) => { + return
{element.name}
+ })} + {!privileges || privileges.length == 0 &&

-

} +
+ + +
+ + +
+ + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer le rôle ?`} + /> + + + ) +} + +export default RoleTableRows \ No newline at end of file diff --git a/src/app/(dashboard)/role/UpdateRoleForm.jsx b/src/app/(dashboard)/role/UpdateRoleForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4b35eadf0d90ee4e0aa60d40c983f0542eebfde3 --- /dev/null +++ b/src/app/(dashboard)/role/UpdateRoleForm.jsx @@ -0,0 +1,143 @@ +import { useNotification } from '@/context/NotificationContext' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import CancelIcon from "@/static/image/svg/cancel.svg" +import { isArray } from '../../lib/TypesHelper' +const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePrivileges, name, id }) => { + const { toggleNotification } = useNotification() + const [loadingStatus, setLoadingStatus] = useState(false) + const [roleName, setRoleName] = useState(name) + const [privileges, setPrivileges] = useState(null) + const [selectedPrivileges, setSelectedPrivileges] = useState(isArray(rolePrivileges) ? rolePrivileges : []) + const inputRef = useRef(null) + console.log("les priv de role ", rolePrivileges) + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + if (isSuccess) { + setPrivileges(data) + } else { + setPrivileges([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getPrivileges() + }, []) + const handleSubmit = async (event) => { + event.preventDefault() + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/roles/${id}/`, { + method: "PATCH", + body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.filter((element) => privileges.find((prvElement) => prvElement.id === element.id)).map((element) => element.id) }) + }) + console.log(data) + setLoadingStatus(false) + if (isSuccess) { + setRoles((roles) => roles.map((element) => element.id === id ? data : element)) + toggleNotification({ + visible: true, + message: "Le rôle a été modifé avec succès", + type: "success" + }) + setRoleToUpdate(null) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le nom de rôle existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle", + type: "warning" + }) + setRoleToUpdate(null) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "Le rôle n'a pas été trouvé", + type: "warning" + }) + setRoleToUpdate(null) + } else if (errors.detail === "Privilege matching query does not exist.") { + toggleNotification({ + visible: true, + message: "Des privilèges que vous avez utilisés ont été supprimés.", + type: "warning" + }) + setRoleToUpdate(null) + } + else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setRoleToUpdate(null) + } + console.log(errors) + } + } + const handleRoleNameChange = (event) => { + setRoleName(event.target.value) + } + const handlePrivilegeClick = (privilege) => { + if (selectedPrivileges.find((element) => element.id === privilege.id)) { + setSelectedPrivileges(selectedPrivileges.filter((element) => element.id !== privilege.id)) + } else { + setSelectedPrivileges([...selectedPrivileges, privilege]) + } + } + const selectAll = () => { + if (privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id))) + setSelectedPrivileges([]) + else setSelectedPrivileges(privileges) + } + var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) + return ( +
+
+ setRoleToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + {(privileges) ?
+

Modification de RĂ´le

+
+ + +
+
+
+ +
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
+
+
+ {privileges.length !== 0 ? privileges?.map((privilege) => { + const isSelected = selectedPrivileges.find((element) => element.id === privilege.id) !== undefined + return
handlePrivilegeClick(privilege)} key={privilege.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}>{privilege.name}
+ }) :
+

Pas encore des habilitations

+
} +
+
+
+ +
+
:
} +
+
+ ) +} + +export default UpdateRoleForm \ No newline at end of file diff --git a/src/app/(dashboard)/role/page.jsx b/src/app/(dashboard)/role/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..dfca5e0015cdcda426b78d1e4bf8375ba0de8701 --- /dev/null +++ b/src/app/(dashboard)/role/page.jsx @@ -0,0 +1,71 @@ +'use client' +import React, { useState, useEffect } from 'react' +import CreateRoleForm from './CreateRoleForm' +import Loader from '@/components/Loader/Loader' +import RoleTableRows from './RoleTableRows' +import fetchRequest from '@/app/lib/fetchRequest' +import { isArray } from '../../lib/TypesHelper' +import AddIcon from "@/static/image/svg/add.svg" +import UpdateRoleForm from './UpdateRoleForm' +import { useNotification } from '@/context/NotificationContext' +const Role = () => { + const [roles, setRoles] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [roleToUpdate, setRoleToUpdate] = useState(null) + const { toggleNotification } = useNotification() + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles") + setIsLoading(false) + if (isSuccess) { + console.log(data) + setRoles(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getRoles() + }, []) + const appendRole = (newRole) => { + setRoles([newRole, ...roles]) + } + return ( +
+ {openCreatePopup && } + {roleToUpdate && } +
+

Liste des RĂ´les

+ +
+ {isLoading &&
} + {!isLoading && <> {(!isArray(roles) || roles?.length === 0) + ?
+

Pas encore des rĂ´les

+
+ :
+ + + + + + + {roles?.map((element) => { + return + })} +
RĂ´leHabilitationsAction
+
+ }} +
+ ) +} + +export default Role \ No newline at end of file diff --git a/src/app/(dashboard)/table/RowTable.jsx b/src/app/(dashboard)/table/RowTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..30acd78b8d16f9932f72a4027989d64e755541c3 --- /dev/null +++ b/src/app/(dashboard)/table/RowTable.jsx @@ -0,0 +1,227 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react'; +import fetchRequest from '../../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + + + + +const RowZone = ({ id, numero, zone, tablesState, zones, filteredPlacesState }) => { + + //states + const [isUpdating, setIsUpdating] = useState(false) + const [tableNum, setTableNum] = useState(numero) + const [selectedZone, setSelectedZone] = useState(zone) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRef = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setTableNum(numero) + setSelectedZone(zone?.id) + selectRef.current.value = zone?.id + inputRef.current.value = numero + }, [numero, zone]) + + const handleUpdateZone = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/tables/${id}/`, { + method: "PATCH", + body: JSON.stringify({ numero: tableNum, id_zone: selectedZone }) + }) + setLoadingStatus(false) + if (isSuccess) { + if (data.message === "NO_CHANGES") { + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } + tablesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) + filteredPlacesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "La table a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numero de la table existe déjà", + visible: true, + }) + } else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numero de la table saisie déjà existe.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la table", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La table n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/tables/${id}/`, { method: "DELETE" }) + if (isSuccess) { + tablesState((prevTableState) => prevTableState.filter((element) => element.id !== id)) + filteredPlacesState((prevTableState) => prevTableState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La table a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La table n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setTableNum(numero) + setSelectedZone(zone.id) + selectRef.current.value = zone.id + inputRef.current.value = numero + } + + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("tableRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + + useEffect(() => { + if (isUpdating && inputRef?.current && selectRef?.current) { + inputRef.current.focus() + selectRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + return ( + + + setTableNum(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la table numero "${numero}"?`} + /> + + ) +} + + +export default RowZone \ No newline at end of file diff --git a/src/app/(dashboard)/table/page.jsx b/src/app/(dashboard)/table/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fa644547ea4fb2f6e5c87d52ddcd228e33c1a010 --- /dev/null +++ b/src/app/(dashboard)/table/page.jsx @@ -0,0 +1,253 @@ +"use client" +import React from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useState, useEffect, useRef } from 'react'; +import Loader from '@/components/Loader/Loader' +import TableIcon from "@/static/image/svg/table.svg" +import { isArray } from '../../lib/TypesHelper' +import RowTable from './RowTable' +import { useNotification } from '@/context/NotificationContext' + + +const Table = () => { + const [tables, setTables] = useState([]) + const [isLoadingData, setIsLoadingData] = useState(true) + const [zones, setZones] = useState([]) + const [etages, setEtages] = useState([]) + const [error, setError] = useState(null) + const [isLoadingAction, setIsLoadingAction] = useState(false) + const [numeroTable, setNumeroTable] = useState(null) + const [selectedZone, setSelectedZone] = useState(null) + const [ selectedEtage, setSelectedEtage ] = useState(null) + const [ filteredTables, setFilteredTables ] = useState([]) + + const { toggleNotification } = useNotification() + + // Fetch data from external API + useEffect(() => { + + function extractAvailableEtagesWithZones(zones) { + const etagesMap = new Map(); + + zones.forEach(zone => { + const etage = zone.id_etage; + const etageKey = `${etage.id}-${etage.numero}`; + + if (!etagesMap.has(etageKey)) { + etagesMap.set(etageKey, { ...etage, zones: [] }); + } + + etagesMap.get(etageKey).zones.push(zone); + }); + + return Array.from(etagesMap.values()); + } + + + const getAllTables = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/tables/', { method: 'GET' }) + if (isSuccess) { + setTables(data) + setFilteredTables(data) + } else { + setTables([]) + setFilteredTables([]) + } + } catch (error) { + console.log(error) + } + } + const getAllZones = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/zones/', { method: 'GET' }) + if (isSuccess) { + setZones(data) + setEtages(extractAvailableEtagesWithZones(data)); + } else { + setZones([]) + setEtages([]) + } + } catch (error) { + console.log(error) + } + } + getAllTables() + getAllZones() + setIsLoadingData(false) + }, []) + console.log("etages", etages) + console.log("zones", zones) + console.log("tables", tables) + const handleSearchingTable = async (e) => { + const numero = e.target.value + try { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/search/table/${numero}`, { method: 'GET' }) + if (isSuccess) { + setTables(data) + setFilteredTables(data) + } else { + setTables([]) + setFilteredTables([]) + } + } catch (error) { + console.log(error) + } + } + + + // create new table section + + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + try{ + const { data, errors, isSuccess } = await fetchRequest("/zoaning/tables/", { + method: "POST", + body: JSON.stringify({ numero: numeroTable, id_zone: selectedZone.id }) + }) + if (isSuccess) { + setIsLoadingAction(false) + setTables((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); + setFilteredTables((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); + setNumeroTable(null) + setSelectedZone(null) + setSelectedEtage(null) + toggleNotification({ + visible: true, + message: "La table a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + if (errors.type === "ValidationError") { + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numéro de la table saisie déjà existe.", + visible: true, + }) + } + } else { + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la table.", + visible: true, + }) + } + console.log(errors) + } + }catch(error){ + console.log(error) + } + } + + + // Handle the name of table change + const handleEtageSelection = (event) => { + const etageId = event.target.value + setFilteredTables(tables.filter(table => table.id_zone.id_etage.id == etageId)) + if(selectedEtage != etageId){ + const selectedEtage = etages.find(etage => etage.id == etageId) + setSelectedEtage(selectedEtage) + setZones(selectedEtage.zones) + setSelectedZone(null) + } + } + + // Handle the name of zone change + const handleZoneSelection = (event) => { + const zoneId = event.target.value + setFilteredTables(tables.filter(table => table.id_zone.id == zoneId)) + if(selectedZone != zoneId){ + const selectedZone = zones.find(zone => zone.id == zoneId) + setSelectedZone(selectedZone) + } + } + + + const handleTableNumero = (event) => { + const numeroTable = event.target.value + setNumeroTable(numeroTable) + } + + return ( +
+
+
+ {!isLoadingData ? + <> +
+

Ajout d'une table

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+

List des Tables

+
+
+ +
+ { handleSearchingTable(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des tables..." required /> +
+
+ {isArray(filteredTables) && filteredTables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? +
+ + + + + + + {filteredTables?.map((element) => { + return + })} +
TableZone-EtageAction
+
+ : +
+

Pas encore des tables

+
} + + : +
+ } +
+
+
+ + ) +} + +export default Table \ No newline at end of file diff --git a/src/app/(dashboard)/user/CreateUserForm.jsx b/src/app/(dashboard)/user/CreateUserForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d4089e84c7325393c666a1bacc4135f6459361c0 --- /dev/null +++ b/src/app/(dashboard)/user/CreateUserForm.jsx @@ -0,0 +1,272 @@ +import React, { useState, useEffect } from 'react' +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import { EMAIL_REGEX } from '../../lib/constants' +import { removeWhiteSpace } from '@/app/lib/StringHelper' +import Select from "react-select"; + + +function generateRandomPassword() { + const length = Math.floor(Math.random() * 3) + 8; // Length between 8 and 10 + const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lowercase = 'abcdefghijklmnopqrstuvwxyz'; + const numbers = '0123456789'; + const symbols = '!@#$%^&*()_+[]{}|;:,.<>?'; + + let password = ''; + + password += uppercase[Math.floor(Math.random() * uppercase.length)]; + password += numbers[Math.floor(Math.random() * numbers.length)]; + password += symbols[Math.floor(Math.random() * symbols.length)]; + + const allCharacters = uppercase + lowercase + numbers + symbols; + + for (let i = 3; i < length; i++) { + const randomIndex = Math.floor(Math.random() * allCharacters.length); + password += allCharacters[randomIndex]; + } + + password = password.split('').sort(() => 0.5 - Math.random()).join(''); + + return password; +} +const CreateUserForm = ({ setIsOpen, appendUser }) => { + const [isLoading, setIsLoading] = useState(false) + const [selectedRole, setSelectedRole] = useState(null) + const { toggleNotification } = useNotification() + const [selectedProject, setSelectedProject] = useState([]) + + const [roles, setRoles] = useState(null) + const [projects, setProjects] = useState(null) + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles/") + if (isSuccess) { + setRoles(data) + } else { + setRoles([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + const getProjects = async () => { + const { isSuccess, errors, data } = await fetchRequest("/projects/"); + if (isSuccess) { + setProjects(data); + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getRoles() + getProjects() + }, []) + + const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) + const [userData, setUserData] = useState({ email: "", password: generateRandomPassword(), first_name: "", last_name: "" }) + const handleFieldChange = (event) => { + setUserData({ ...userData, [event.target.name]: event.target.value }) + setErrors({ ...errors, [event.target.name]: "" }) + } + useEffect(() => { + setUserData({ + ...userData, email: (userData.last_name || userData.first_name) ? removeWhiteSpace(userData.first_name + "." + userData.last_name + "@teamwillgroup.com") : "" + }) + setErrors({ ...errors, email: "" }) + }, [userData.first_name, userData.last_name]) + const isValidFields = () => { + const localErrors = { first_name: "", last_name: "", email: "", role: "" } + if (userData.first_name === "") localErrors.first_name = "Le prénom doit être spécifier." + if (userData.last_name === "") localErrors.last_name = "Le nom doit être spécifier." + if (!selectedRole) localErrors.role = "Le rôle doit être spécifier." + if (!userData.email) localErrors.email = "L'email doit être spécifier." + else if (!EMAIL_REGEX.test(userData.email)) localErrors.email = "Votre adresse email n'est pas valide." + setErrors({ ...localErrors }) + return Object.values(localErrors).find((element) => element !== "") === undefined + } + const handleSubmit = async (event) => { + event.preventDefault() + if (isValidFields()) { + setIsLoading(true) + const { data, errors: requestErrors, isSuccess, status } = await fetchRequest("/users/", { + method: "POST", + body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole, projects: selectedProject }) + }) + if (isSuccess) { + setIsLoading(false) + setSelectedRole(null) + appendUser(data) + toggleNotification({ + visible: true, + message: "L'utilisateur a été créé avec succès", + type: "success" + }) + setIsOpen(false) + } else { + setIsLoading(false) + if (requestErrors.type === "ValidationError") { + if (requestErrors.detail.email) { + setErrors({ ...errors, email: "Cette adresse email est déjà utilisée." }) + } + else if (requestErrors.detail.role) { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle (le rôle peut être supprimé)", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de utilisateur", + type: "warning" + }) + setIsOpen(false) + } + } + else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setIsOpen(false) + } + console.log(requestErrors) + } + } + + } + const handleRoleChange = (event) => { + setErrors({ ...errors, role: "" }) + if (event.target.value) + setSelectedRole(event.target.value) + else setSelectedRole(null) + } + const handleProjectChange = (selectedOptions) => { + setSelectedProject(selectedOptions.map((element) => element.value)) + } + return ( +
+
+ setIsOpen(false)} className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600" /> + {(roles && projects) ?
+

Ajout d'utilisateur

+
+
+ + +

{errors.last_name}

+
+
+ + +

{errors.first_name}

+
+
+
+ + +

{errors.email}

+
+
+ + +
+
+
+ +
+
+ {projects.length !== 0 ? +
+ + + {roles?.map((role) => { + return + })} + +

{errors.role}

+
+ :
+

Pas encore des rĂ´les

+
} +
+
+
+ +
+
:
} +
+
+ ) +} + +export default CreateUserForm \ No newline at end of file diff --git a/src/app/(dashboard)/user/UpdateUserForm.jsx b/src/app/(dashboard)/user/UpdateUserForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ff36c1ed52db07dccc39ea4e249953bbca91c3d5 --- /dev/null +++ b/src/app/(dashboard)/user/UpdateUserForm.jsx @@ -0,0 +1,244 @@ +import React, { useState, useEffect, useMemo } from 'react' +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import { EMAIL_REGEX } from '../../lib/constants' +import { removeWhiteSpace } from '@/app/lib/StringHelper' +import Select from "react-select"; + +const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => { + const [isLoading, setIsLoading] = useState(false) + const [selectedRole, setSelectedRole] = useState(userToUpdate.role?.id || null) + const { toggleNotification } = useNotification() + const [selectedProject, setSelectedProject] = useState(userToUpdate.projects?.length ? [userToUpdate.projects[0].id] : []) + console.log(userToUpdate); + const [roles, setRoles] = useState(null) + const [projects, setProjects] = useState(null) + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles/") + if (isSuccess) { + setRoles(data) + } else { + setRoles([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + const getProjects = async () => { + const { isSuccess, errors, data } = await fetchRequest("/projects/"); + if (isSuccess) { + setProjects(data); + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getRoles() + getProjects() + }, []) + const handleFieldChange = (event) => { + setUserData({ ...userData, [event.target.name]: event.target.value }) + setErrors({ ...errors, [event.target.name]: "" }) + } + const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) + const [userData, setUserData] = useState({ email: userToUpdate.email, first_name: userToUpdate.first_name, last_name: userToUpdate.last_name }) + useEffect(() => { + setUserData({ + ...userData, email: (userData.last_name || userData.first_name) ? removeWhiteSpace(userData.first_name + "." + userData.last_name + "@teamwillgroup.com") : "" + }) + setErrors({ ...errors, email: "" }) + }, [userData.first_name, userData.last_name]) + const isValidFields = () => { + const localErrors = { first_name: "", last_name: "", email: "", role: "" } + if (userData.first_name === "") localErrors.first_name = "Le prénom doit être spécifier." + if (userData.last_name === "") localErrors.last_name = "Le nom doit être spécifier." + if (!selectedRole) localErrors.role = "Le rôle doit être spécifier." + if (!userData.email) localErrors.email = "L'email doit être spécifier." + else if (!EMAIL_REGEX.test(userData.email)) localErrors.email = "Votre adresse email n'est pas valide." + setErrors({ ...localErrors }) + return Object.values(localErrors).find((element) => element !== "") === undefined + } + const handleSubmit = async (event) => { + event.preventDefault() + if (isValidFields()) { + setIsLoading(true) + const { data, errors: requestErrors, isSuccess, status } = await fetchRequest(`/users/${userToUpdate.id}/`, { + method: "PATCH", + body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole, projects: selectedProject }) + }) + if (isSuccess) { + setUsers((users) => users.map((element) => element.id === userToUpdate.id ? data : element)) + setIsLoading(false) + setSelectedRole(null) + toggleNotification({ + visible: true, + message: "L'utilisateur a été modifié avec succès", + type: "success" + }) + setUserToUpdate(null) + } else { + setIsLoading(false) + if (requestErrors.type === "ValidationError") { + if (requestErrors.detail.email) { + setErrors({ ...errors, email: "Cette adresse email est déjà utilisée." }) + } + else if (requestErrors.detail.role) { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle (le rôle peut être supprimé)", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de utilisateur", + type: "warning" + }) + setUserToUpdate(null) + } + } + else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setUserToUpdate(null) + } + console.log(requestErrors) + } + } + + } + const handleRoleChange = (event) => { + setErrors({ ...errors, role: "" }) + if (event.target.value) + setSelectedRole(event.target.value) + else setSelectedRole(null) + } + const handleProjectChange = (selectedOptions) => { + setSelectedProject(selectedOptions.map((element) => element.value)) + } + const projectsDefaultValue = useMemo(() => userToUpdate.projects.map((project) => ({ label: project.nom, value: project.id })), []) + return ( +
+
+ setUserToUpdate(null)} className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600" /> + {(roles && projects) ?
+

Modification d'utilisateur

+
+
+ + +

{errors.last_name}

+
+
+ + +

{errors.first_name}

+
+
+
+ + +

{errors.email}

+
+
+
+ +
+
+ {projects.length !== 0 ? +
+ + + {roles?.map((role) => { + return + })} + +

{errors.role}

+
+ :
+

Pas encore des rĂ´les

+
} +
+
+
+ +
+
:
} +
+
+ ) +} + +export default UpdateUserForm \ No newline at end of file diff --git a/src/app/(dashboard)/user/UserTableRow.jsx b/src/app/(dashboard)/user/UserTableRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..da0b5aac685e30b5cba3d0312f81894ff6415e7b --- /dev/null +++ b/src/app/(dashboard)/user/UserTableRow.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import ConfirmationModal from '../../ui/ConfirmationModal' +import { useNotification } from '@/context/NotificationContext' +import fetchRequest from '../../lib/fetchRequest' + +const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projects, setUserToUpdate }) => { + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + const showDeletePopup = () => { + setModalOpen(true); + } + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/users/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setUsers((users) => users.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "L'utilisateur a été supprimé avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "L'utilisateur n'a pas été trouvé", + type: "warning" + }) + } else if (errors.detail?.indexOf("Cannot delete some instances of model") !== -1) { + toggleNotification({ + visible: true, + message: "Impossible de supprimer cet utilisateur car il est attribué à d'autre objets", + type: "warning" + }) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + setModalOpen(false) + console.log(errors) + } + return ( + <> + + +

{first_name} {last_name}

+ + +

{email}

+ + +

{role?.name || ""}

+ + +
    + {(!projects || projects.length === 0) &&

    -

    } + {projects?.map((project) => { + return
  • {project.nom}
  • + })} +
+ + +
+ + +
+ + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer le rĂ´le ?`} + /> + + ) +} + +export default UserTableRow \ No newline at end of file diff --git a/src/app/(dashboard)/user/page.jsx b/src/app/(dashboard)/user/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d58b7af418d2f118165a5eb005b2bccdc06c9934 --- /dev/null +++ b/src/app/(dashboard)/user/page.jsx @@ -0,0 +1,117 @@ +'use client'; +import React, { useEffect, useState } from 'react' +import CreateUserForm from './CreateUserForm' +import UpdateUserForm from './UpdateUserForm' +import AddIcon from "@/static/image/svg/add.svg" +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../../lib/fetchRequest'; +import { isArray } from '../../lib/TypesHelper'; +import { useNotification } from '@/context/NotificationContext'; +import UserTableRow from './UserTableRow'; +import { PAGINATION_SIZE } from '../../lib/constants'; +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" +const UserPage = () => { + const [users, setUsers] = useState([]) + const [paginationData, setPaginationData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [userToUpdate, setUserToUpdate] = useState(null) + const { toggleNotification } = useNotification() + const [query, setQuery] = useState(''); + const getUsers = async (pageNumber = 1, signal) => { + setIsLoading(true) + if (search) var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}&search=${query}`, { signal: signal }) + else var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}`) + setIsLoading(false) + if (isSuccess) { + console.log(data) + setUsers(data.results) + setPaginationData({ pagesNumber: Math.ceil((data.count || 0) / PAGINATION_SIZE), currentPage: pageNumber }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + useEffect(() => { + const controller = new AbortController() + const signal = controller.signal + getUsers(1, signal) + return () => { + controller.abort("fetching another users") + } + }, [query]) + const appendUser = (newUser) => { + setUsers([newUser, ...users]) + } + + const handleSearchChange = (event) => { + setQuery(event.target.value) + } + return ( +
+ {openCreatePopup && } + {userToUpdate && } +
+ +
+
+

Liste des Utilisateurs

+ +
+ {(isLoading) &&
} + {(!isLoading) && <> {(!isArray(users) || users?.length === 0) + ?
+

Pas encore des utilisateurs

+
+ : +
+ + + + + + + + + {users?.map((element) => { + return + })} + +
NomEmailRĂ´leProjetsAction
+
} + + } +
+ {(paginationData) &&
+ {(paginationData.currentPage > 1) &&
getUsers(paginationData.currentPage - 1)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> + +
} + {paginationData && Array.from({ length: paginationData.pagesNumber }, (_, index) => index).map((element, index) => { + if (element + 1 === paginationData.currentPage + 3) return

+ ... +

+ else if (paginationData.currentPage === element + 1) return

+ {element + 1} +

+ else if (paginationData.currentPage < element + 3 && element - paginationData.currentPage < 3) return

getUsers(element + 1)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center' > + {element + 1} +

+ else return <> + })} + {(paginationData.currentPage !== paginationData.pagesNumber) &&
getUsers(paginationData.currentPage + 1)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> + +
} +
} +
+
+ ) +} + +export default UserPage \ No newline at end of file diff --git a/src/app/(dashboard)/zone-project/AssignProject.jsx b/src/app/(dashboard)/zone-project/AssignProject.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3c923a83f0492ba33864c5cedcb94cf62bc572dd --- /dev/null +++ b/src/app/(dashboard)/zone-project/AssignProject.jsx @@ -0,0 +1,366 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/study-desk.svg" +import fetchRequest from "@/app/lib/fetchRequest"; + + +const AssignProject = ({ setIsOpen, listProjects, affectations, mutateProjectAffection }) => { + const [loading, setLoading] = useState(false) + const [projects, setProjects] = useState([]) + const [zones, setZones] = useState([]) + const [ selectedDay, setSelectedDay ] = useState('') + const [ selectedWeek, setSelectedWeek ] = useState('') + const [ selectedZone, setSelectedZone ] = useState(null) + const [ selectedProject, setSelectedProject ] = useState(null) + const [ places , setPlaces ] = useState([]) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + const [ otherPlaces, setOtherPlaces ] = useState([]) + + const { toggleNotification } = useNotification() + + const attributedCollabsRef = useRef() + useEffect(() => { + const fetchProjectsandZones = async () => { + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${selectedWeek}/${selectedDay}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setNbrCollabs(0) + setPlaces([]) + if ( (data.projects && data.projects.length === 0) && (data.zones && data.zones.length === 0)){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets et de zones pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + setZones([]) + }else{ + if(data.projects && data.projects.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + }else{ + setProjects(data.projects) + } + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + setZones(data.zones) + } + } + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + toggleNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + if(selectedDay && selectedWeek) fetchProjectsandZones() + }, [selectedDay, selectedWeek]) + + + const handleZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) + if(isSuccess){ + setSelectedZone(zone_id) + setPlaces(data.places) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + const handleProjectSelection = async (e) => { + const project_id = e.target.value + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingCollabs/${project_id}`, {method: 'GET'}) + if(isSuccess){ + setSelectedProject(project_id) + setNbrCollabs(data.user_count) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setNbrCollabs(0) + } + }catch(error){ + console.log(error) + } + } + + useEffect( () => { + if(nbrCollabs > 0 && places.length > 0){ + if( nbrCollabs <= places.length){ + setCollabsAttributed(nbrCollabs) + attributedCollabsRef.current = nbrCollabs + }else{ + setCollabsAttributed(places.length) + attributedCollabsRef.current = places.length + } + } + }, [nbrCollabs, places]) + + + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + attributedCollabsRef.current = collabsAttributed + 1 + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + attributedCollabsRef.current = collabsAttributed - 1 + } + + + + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) + if(isSuccess){ + setOtherPlaces(data.places) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + useEffect( () => { + if( (collabsAttributed - places.length) <= 0 && selectedOtherZone ) { + setSelectedOtherZone(null) + setOtherPlaces([]) + } + }, [collabsAttributed]) + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject, + jour: selectedDay, + semaine: selectedWeek, + nombre_personnes: nbrCollabs, + places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, + places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, + places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + } + if( selectedOtherZone && otherPlaces.length > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, + places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + } + } + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + const newAffectations = [...affectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listProjects(newAffectations) + mutateProjectAffection(newAffectations) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const closePopup = () => { + setIsOpen() + setSelectedWeek('') + setSelectedDay('') + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setPlaces([]) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherPlaces([]) + setProjects([]) + setZones([]) + setLoading(false) + + + } + + + return ( +
+
+

Attribuer un projet Ă  une zone.

+ {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
+
+

Veuillez sélectionner une date

+
+
+ +
+
+ +
+
+
+
+
+ {(!loading && (projects && projects.length)) ? + + : +
+ Aucun projet disponible +
+ } +
+

: {nbrCollabs}

+
+
+
+ {(!loading && (zones && zones.length)) ? + + : +
+ Aucune zone disponible +
+ } +
+

: {places.length}

+
+
+ {collabsAttributed > 0 && + <> +
+ Collaborateurs attribués +
+ {collabsAttributed} +
+ + +
+
+
+
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+ restant +
+
+ {((collabsAttributed - places.length) > 0) &&
+

Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

+
+ +
+

: {otherPlaces.length}

+
+
+
} + + } +
+ +
+
+
+
+
+ ) +} + +export default AssignProject diff --git a/src/app/(dashboard)/zone-project/CompleteAffectation.jsx b/src/app/(dashboard)/zone-project/CompleteAffectation.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d59137ad940daaa60634c2c4e57d1c090faf554f --- /dev/null +++ b/src/app/(dashboard)/zone-project/CompleteAffectation.jsx @@ -0,0 +1,300 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/study-desk.svg" +import AddIcon from "@/static/image/svg/add.svg" +import fetchRequest from "@/app/lib/fetchRequest"; +import Loader from '@/components/Loader/Loader' + + + +const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, fullAffectations, mutateProjectAffection }) => { + const [ selectedProject, setSelectedProject ] = useState(null) + const [ zones, setZones ] = useState([]) + const [ loading, setLoading ] = useState(false) + const [ places , setPlaces ] = useState([]) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const { toggleNotification } = useNotification() + const [ selectedZone, setSelectedZone ] = useState(null) + const [ otherPlaces, setOtherPlaces ] = useState([]) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + + + const getZones = async (day, week) => { + console.log("day, week", day, week) + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${day}/${week}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setPlaces([]) + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + console.log("dsqqqqqdsqdqs") + setZones(data.zones) + } + + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + toggleNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + + const handleProjectSelection = (project) => { + if(selectedProject && selectedProject.project.id === project.project.id){ + setSelectedProject(null) + setZones([]) + setPlaces([]) + setCollabsAttributed(0) + return + } + console.log(project) + setSelectedProject(project) + getZones(project.jour, project.semaine) + setNbrCollabs(project.nbr_personnes_restant) + } + + + useEffect( () => { + if(nbrCollabs > 0 && places.length > 0){ + if( nbrCollabs <= places.length){ + setCollabsAttributed(nbrCollabs) + }else{ + setCollabsAttributed(places.length) + } + } + }, [nbrCollabs, places]) + + const handleSelectionZone = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.week && element.id_zone.id == zone_id).map(element => element.id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) + if(isSuccess){ + console.log(data.places) + setSelectedZone(zone_id) + setPlaces(data.places) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.semaine && element.id_zone.id == zone_id).map(element => element.id) + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) + if(isSuccess){ + setOtherPlaces(data.places) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + } + console.log("projectss", affectations) + console.log("selected project", selectedProject) + + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject.project.id, + jour: selectedProject.jour, + semaine: selectedProject.semaine, + nombre_personnes: selectedProject.nombre_personnes, + places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, + places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, + places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + } + if( selectedOtherZone && otherPlaces.length > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, + places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + } + } + console.log(finalData) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + const newAffectations = [...fullAffectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listAffectationsState(newAffectations) + mutateProjectAffection(newAffectations) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + + const closePopup = () => { + setIsOpen() + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setPlaces([]) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherPlaces([]) + setZones([]) + setLoading(false) + } + + console.log(nbrCollabs) + + return ( +
+
+

Compléter l'affectation

+ {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
+
+

Veuillez sélectionner un projet

+
+ {(affectations) && + affectations.map((element, index) => +
handleProjectSelection(element)} key={index} className='border-b border-gray-200'> + Projet: {element?.project?.nom} -- Collaborateurs: {element.nbr_personnes_restant} +
+ ) + } +
+
+
+

Veuillez sélectionner une zone

+
+ {(!loading) ? + (zones && zones.length) ? + + : +
+ Aucune zone disponible +
+ : +
+ +
+ + } +
+

: {places.length}

+
+
+
+ {(collabsAttributed > 0) &&
+

Collaborateurs affectées

+
+
+ {(collabsAttributed) ? collabsAttributed : 0} +
+ + +
+
+
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
+ 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
+
+
+
} + {((collabsAttributed - places.length) > 0) &&
+

Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

+
+ +
+

: {otherPlaces.length}

+
+
+
} +
+
+ +
+
+
+ ) +} + +export default CompleteAffectation diff --git a/src/app/(dashboard)/zone-project/page.jsx b/src/app/(dashboard)/zone-project/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c6de000c4b85d511551a4ff423579fb23a42d6e4 --- /dev/null +++ b/src/app/(dashboard)/zone-project/page.jsx @@ -0,0 +1,341 @@ +"use client" +import React, { useEffect, useState } from 'react'; +import AddIcon from "@/static/image/svg/add.svg"; +import AssignProject from './AssignProject'; +import { useNotification } from '@/context/NotificationContext' + +import Loader from '@/components/Loader/Loader' +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; +import fetchRequest from "@/app/lib/fetchRequest"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import CompleteAffectation from './CompleteAffectation'; + + + + +const AffectingZoneProject = () => { + const [isOpen, setIsOpen] = useState(false) + const [isOpenCompleteAffectation, setIsOpenCompleteAffectation] = useState(false) + const [listProjectsAffected, setListProjectsAffected] = useState([]) + const [isLoadingListProjects, setIsLoadingListProjects] = useState(false) + const { toggleNotification } = useNotification() + const [selectedWeek, setSelectedWeek] = useState(null) + const [selectedDay, setSelectedDay] = useState(null) + const [selectedAffectaionToDelete, setSelectedAffectationToDelete] = useState(null) + const [isModalOpen, setModalOpen] = useState(false); + const [listProjectsSemiAffected, setListProjectsSemiAffected] = useState([]) + + + useEffect(() => { + + // this function is to detect if there is a project that is not fully affected and return the corresponding data to use + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.nombre_personnes + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.nombre_personnes - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.nombre_personnes - project.places_occuper, + nombre_personnes: project.nombre_personnes + }; + }); + + return result; + } + + const getListOfAffectedProjects = async () => { + setIsLoadingListProjects(true) + try { + if (selectedDay && selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedDay) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, { method: 'GET' }) + } else { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, { method: 'GET' }) + } + if (isSuccess) { + setListProjectsAffected(data) + console.log("this is our ", data); + setListProjectsSemiAffected(filterAndGroupProjects(data)) + } else { + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + setIsLoadingListProjects(false) + } catch (error) { + setIsLoadingListProjects(false) + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getListOfAffectedProjects() + }, [selectedDay, selectedWeek]) + + + const handleOpenAssignProject = () => { + setIsOpen(!isOpen) + } + + const handleOpenCompleteAffectation = () => { + setIsOpenCompleteAffectation(!isOpenCompleteAffectation) + } + + const handleDeleteAffectation = async () => { + try { + var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, { method: 'DELETE' }) + if (isSuccess) { + toggleNotification({ + visible: true, + message: "Affectation supprimer avec succès", + type: "success" + }) + const filteredProjectsAffected = listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id) + setListProjectsAffected(filteredProjectsAffected) + mutateProjectsAffectaionCheck(filteredProjectsAffected) + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "Affectation introuvable", + type: "error" + }) + } else { + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + } catch (error) { + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const handleDeleteClick = (element) => { + setSelectedAffectationToDelete(element) + setModalOpen(true); + } + + + const handleConfirmDelete = () => { + handleDeleteAffectation(); + setModalOpen(false); + setSelectedAffectationToDelete(null); + + }; + + // function to detect if there is a project not fully affected ( outside the use effect to mutate the changement of the state) + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.nombre_personnes + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.nombre_personnes - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.nombre_personnes - project.places_occuper, + nombre_personnes: project.nombre_personnes + }; + }); + + return result; + } + + const mutateProjectsAffectaionCheck = (passedData) => { + console.log("mutaion triggered") + setListProjectsSemiAffected(filterAndGroupProjects(passedData)) + } + + console.log("project semi affected", listProjectsSemiAffected) + console.log("project fully associated", listProjectsAffected) + + + return ( +
+
+ {isOpen && } + {(isOpenCompleteAffectation) && } +

List des Projets attribuer

+
+ +
+ +
+
+ +
+ {(!isLoadingListProjects) ? + (listProjectsAffected && listProjectsAffected.length > 0) ? + + + + + + + + + + + + + + {(listProjectsAffected.map((element, index) => + + {/* */} + + + + + + + + + ))} + +
+ Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions +
+ Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + +
handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
+
+ : +
+ Aucun projet affecter +
+ : +
+ } + { + (listProjectsSemiAffected && listProjectsSemiAffected.length) ?
+ +
+ : + "" + } + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'affectation "${selectedAffectaionToDelete?.id_project.nom}"?`} + /> +
+ + ); +} + + +export default AffectingZoneProject; diff --git a/src/app/(dashboard)/zone/RowZone.jsx b/src/app/(dashboard)/zone/RowZone.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eac3a4cc6a29bbcbf676e097a3a2cb439d53f9fe --- /dev/null +++ b/src/app/(dashboard)/zone/RowZone.jsx @@ -0,0 +1,226 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react'; +import fetchRequest from '../../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + + + + +const RowZone = ({ id, nom, etage, zonesState, etages }) => { + //states + const [isUpdating, setIsUpdating] = useState(false) + const [zoneName, setZoneName] = useState(nom) + const [selectedEtage, setSelectedEtage] = useState(etage) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRef = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setZoneName(nom) + setSelectedEtage(etage?.id) + selectRef.current.value = etage?.id + inputRef.current.value = nom + }, [nom, etage]) + + const handleUpdateZone = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/zones/${id}/`, { + method: "PATCH", + body: JSON.stringify({ nom: zoneName, id_etage: selectedEtage }) + }) + setLoadingStatus(false) + if (isSuccess) { + console.log(data) + if (data.message === "NO_CHANGES") { + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } + console.log(data.data) + zonesState((prevZonesValue) => prevZonesValue.map((element) => element.id === id ? { ...data.data, id_etage: etages.find(etage => etage.id === data.data.id_etage) } : element)) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "La zone a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.nom) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone existe déjà", + visible: true, + }) + } else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone saisie déjà existe.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la zone", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La zone n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/zones/${id}/`, { method: "DELETE" }) + if (isSuccess) { + zonesState((prevZonesState) => prevZonesState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La zone a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La zone n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setZoneName(nom) + setSelectedEtage(etage.id) + selectRef.current.value = etage.id + inputRef.current.value = nom + } + + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("zoneRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + + useEffect(() => { + if (isUpdating && inputRef?.current && selectRef?.current) { + inputRef.current.focus() + selectRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + return ( + + + setZoneName(event.target.value)} defaultValue={nom} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la zone "${nom}"?`} + /> + + ) +} + + +export default RowZone \ No newline at end of file diff --git a/src/app/(dashboard)/zone/page.jsx b/src/app/(dashboard)/zone/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..67764d39af69c17c3d11625dfab7c44db8bd597e --- /dev/null +++ b/src/app/(dashboard)/zone/page.jsx @@ -0,0 +1,180 @@ +"use client" +import React from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useState, useEffect, useRef } from 'react'; +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { isArray } from '../../lib/TypesHelper' +import RowZone from './RowZone' +import { useNotification } from '@/context/NotificationContext' + + +const Zone = () => { + const [zones, setZones] = useState([]) + const [isLoadingData, setIsLoadingData] = useState(true) + const [etages, setEtages] = useState([]) + const [error, setError] = useState(null) + const [isLoadingAction, setIsLoadingAction] = useState(false) + const [nomZone, setNomZone] = useState(null) + const [selectedEtage, setSelectedEtage] = useState(null) + + const inputRef = useRef(null) + const selectRef = useRef(null) + + const { toggleNotification } = useNotification() + + + // Fetch data from external API + useEffect(() => { + const getAllZones = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/zones/', { method: 'GET' }) + if (isSuccess) { + setZones(data) + } else { + setZones([]) + } + } catch (error) { + console.log(error) + } + } + const getAllEtages = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + if (isSuccess) { + setEtages(data) + } else { + setEtages([]) + } + } catch (error) { + console.log(error) + } + } + getAllZones() + getAllEtages() + setIsLoadingData(false) + }, []) + + + // create zone section + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/zones/", { + method: "POST", + body: JSON.stringify({ nom: nomZone, id_etage: selectedEtage }) + }) + if (isSuccess) { + setIsLoadingAction(false) + setZones((prevZoneValue) => [...prevZoneValue, { ...data, id_etage: etages.find(etage => etage.id === data.id_etage) }]); + inputRef.current.value = "" + selectRef.current.value = "" + setNomZone(null) + setSelectedEtage(null) + toggleNotification({ + visible: true, + message: "La zone a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + if (errors.type === "ValidationError") { + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone saisie déjà existe.", + visible: true, + }) + } + } else { + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la zone.", + visible: true, + }) + } + console.log(errors) + } + } + + // Handle the name of zone change + const handleChangeZone = (event) => { + setError("") + setNomZone(event.target.value) + } + + const handleChangeEtage = (event) => { + setError("") + setSelectedEtage(event.target.value) + } + + console.log(zones) + console.log(etages) + + return ( +
+
+
+ {!isLoadingData ? + <> +
+

Ajout d'une zone

+
+
+ + +
+
+ + +
+
+ +
+
+
+

List des Zones

+ {isArray(zones) && zones?.length !== 0 && isArray(etages) && etages?.length !== 0 ? +
+ + + + + + + {(selectedEtage) ? + zones?.filter(zone => zone.id_etage.id == selectedEtage).map((element) => {return }) + : + zones?.map((element) => { + return + })} +
ZoneEtageAction
+
+ : +
+

Pas encore des zones

+
} + + : +
+ } +
+
+
+ + ) +} + +export default Zone \ No newline at end of file diff --git a/src/app/auth/change-password/page.jsx b/src/app/auth/change-password/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6bc46caa9f1a0aa71d39d43d249f2fb50b0bbafd --- /dev/null +++ b/src/app/auth/change-password/page.jsx @@ -0,0 +1,126 @@ +"use client" +import fetchRequest from '@/app/lib/fetchRequest' +import Image from 'next/image' +import Link from 'next/link' +import React, { useEffect, useState } from 'react' +import { useSearchParams } from 'next/navigation' +import Loader from '@/components/Loader/Loader' +import { PASSWORD_REGEX } from '@/app/lib/constants' +import { useNotification } from '@/context/NotificationContext' + +const ChangePassword = () => { + const [isSuccess, setIsSuccess] = useState(false) + const [formErrors, setFormErrors] = useState([]) + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const params = useSearchParams(); + const { toggleNotification } = useNotification() + const handleChangePassword = async (event) => { + event.preventDefault() + setIsLoading(true) + const { isSuccess, data, errors } = await fetchRequest(`/password_reset/confirm/?token=${params.get("token")}`, { + method: "POST", + body: JSON.stringify({ + password, + token: params.get("token") + }) + }) + if (isSuccess) { + setIsSuccess(true) + } + else { + console.log(errors) + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.token) { + setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est invalide"]) + } + else { + setFormErrors(["Le Mot de passe est invalide"]) + } + } else if (errors?.detail?.detail?.startsWith("The OTP password entered is not valid")) { + setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est déja utilisé"]) + } + else { + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + } + const isEmptyFields = !password && !confirmPassword + useEffect(() => { + var currentErrors = [] + if (password !== confirmPassword) { + currentErrors.push("Les mots de passe ne sont pas identiques.") + } + if (password.length < 8) { + currentErrors.push("Le mot de passe doit comporter au moins 8 caractères.") + } + if (!PASSWORD_REGEX.test(password)) { + currentErrors.push("Le mot de passe doit contenir au moins une lettre, un chiffre et un caractère spécial.") + } + setFormErrors(currentErrors) + }, [password, confirmPassword]) + return ( +
+
+
+
+ teamwill +
+ {(!isSuccess) &&
+

Change your password. +

+
+
    +
  • Le mot de passe doit contenir au moins 8 caractères
  • +
  • Le mot de passe doit contenir au moins un chiffre
  • +
  • Le mot de passe doit contenir au moins un caractère spĂ©cial
  • +
+
+
+
+ + setPassword(event.target.value)} type="password" name="new_password1" id="new_password1" + className="rounded-md px-3 w-full duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" /> +
+
+ + setConfirmPassword(event.target.value)} type="password" name="new_password2" id="new_password2" + className="rounded-md px-3 duration-150 delay-75 w-full focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" /> +
+
    0 && !isEmptyFields ? "bg-red-100 border border-red-300" : ""} min-h-10 px-3 text-xs py-3 rounded relative mt-9 mb-6 list-inside list-disc`} role="alert"> + {!isEmptyFields && formErrors.map((error, index) => { + return
  • {error}
  • + })} +
+
+ +
+
+
} + {(isSuccess) && ( +
+

The password has been changed!

+ log in again? +
+ )} +
+
+
+ ) +} + +export default ChangePassword \ No newline at end of file diff --git a/src/app/auth/forgot-password/page.jsx b/src/app/auth/forgot-password/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d74d5db9ab4b3f3bd45be27257ce6fead4b790b3 --- /dev/null +++ b/src/app/auth/forgot-password/page.jsx @@ -0,0 +1,105 @@ +'use client' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import Image from 'next/image' +import Link from 'next/link' +import React, { useState } from 'react' + +const ForgotPassword = () => { + const [email, setEmail] = useState("") + const [isSuccess, setIsSuccess] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [requestErrors, setRequestErrors] = useState([]) + const { toggleNotification } = useNotification() + const handleForgetPassword = async (event) => { + event.preventDefault() + setIsLoading(true) + const { isSuccess, errors, data } = await fetchRequest("/password_reset/", { + body: JSON.stringify({ email }), + method: "POST" + }) + if (isSuccess) { + setIsSuccess(true) + } + else { + setIsLoading(false) + if (errors.type === "ValidationError") { + // setRequestErrors(Object.keys(errors.detail).reduce((prev, current) => { + // return [...prev, ...errors.detail[current]] + // }, [])) + toggleNotification + setRequestErrors(["Nous n'avons pas pu trouver de compte associé à cet e-mail. Veuillez essayer une autre adresse e-mail."]) + } else { + console.log(errors) + toggleNotification({ + type: "error", + message: "Internal Server Error" + }) + } + } + } + return ( +
+
+
+ teamwill + {(!isSuccess) &&
+
+

Forgot your password!! No Problem + Reset it here +

+
+
+
+
+
+ setEmail(e.target.value)} autocomplete="off" id="email" name="email" type="text" + className="peer placeholder-transparent h-10 w-full border-b-2 border-gray-300 text-gray-900 focus:outline-none focus:borer-rose-600" + placeholder="Email address" /> + +
+ + + {(requestErrors.length !== 0) && } +
+ Back to log + in +
+ +
+
+
+
} + {(isSuccess) &&
+ + + +
+

Email Sent!

+

Check your email and open the link we sent to continue

+
+
} +
+
+
+ ) +} + +export default ForgotPassword \ No newline at end of file diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 33b3f092806c43146c129778fdef8fa71449bd18..2bdb9a070baaa5983da96f4d5185e8c7919b7d6e 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -3,12 +3,16 @@ import { useState } from 'react'; import Link from 'next/link'; import Cookies from 'js-cookie'; -import {createSession} from "@/app/lib/session"; +import { createSession } from "@/app/lib/session"; +import { useRouter } from 'next/navigation' +import Loader from '@/components/Loader/Loader'; const LoginPage = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [messages, setMessages] = useState(''); + const [isLoading, setIsLoading] = useState(false) + const router = useRouter(); const handleSubmit = (event) => { event.preventDefault(); @@ -18,45 +22,11 @@ const LoginPage = () => { // Add your form submission logic here }; - const secretKey = process.env.NEXT_PUBLIC_SESSION_SECRET; - console.log(secretKey); - - // send login request to the server const login = async (event) => { event.preventDefault(); try { - const response = await fetch('http://localhost:8000/login/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username, password }), - }); - const data = await response.json(); - if (data.error) { - setMessages(data.error); - } else { - setMessages('Login successful'); - // Set the cookie - const expiresAt = new Date(new Date().getTime() + 60 * 60 * 1000); // Set cookie to expire in 1 hour - // const session = json strigify it - Cookies.set('session', data.token, { - expires: expiresAt, - secure: true, - sameSite: 'Lax', - }); - // Redirect to the dashboard - // window.location.href = '/auth/verif'; - } - } catch (error) { - setMessages('An error occurred'); - } - }; - - const loginn = async (event) => { - event.preventDefault(); - try { - const response = await fetch('http://localhost:8000/login/', { + setIsLoading(true) + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login/`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -64,16 +34,13 @@ const LoginPage = () => { body: JSON.stringify({ username, password }), }); const data = await response.json(); - if (data.error) { - setMessages(data.error); + console.log(data) + setIsLoading(false) + if (data.detail?.non_field_errors) { + setMessages("Email ou mot de passe incorrect. Veuillez réessayer."); } else { - setMessages('Login successful'); - // Set the cookie - const expiresAt = new Date(new Date().getTime() + 60 * 60 * 1000); // Set cookie to expire in 1 hour - // const session = json strigify it await createSession(data); - // Redirect to the dashboard - // window.location.href = '/auth/verif'; + router.push('/'); } } catch (error) { setMessages('An error occurred'); @@ -91,7 +58,7 @@ const LoginPage = () => {
{
{ />
- -

Mot de passe oublié?

+ +

Mot de passe oublié?

{messages && ( @@ -123,10 +90,11 @@ const LoginPage = () => {

)}
diff --git a/src/app/auth/verif/page.jsx b/src/app/auth/verif/page.jsx index cfea4df1253203ecab55cdbcb6d5644d8980cac5..15d3d4083be7e343e184e71bde68c51c9ab26e99 100644 --- a/src/app/auth/verif/page.jsx +++ b/src/app/auth/verif/page.jsx @@ -5,7 +5,7 @@ const Verif = () => { // fetch data from the server /example const fetchExampleData = async () => { try { - const data = await fetchRequest('http://localhost:8000/example/', + const data = await fetchRequest(`/example/`, {method: 'POST', body: JSON.stringify({ "name": "jhon", @@ -20,18 +20,18 @@ const Verif = () => { } }; const isAuth = async () => { - isAuthenticated(); + await isAuthenticated(); } return ( - <> +
user is redirected to this page after successful login
{/*test fetchExampleData */} - +
); } export default Verif; \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index 6c139df80533021db2ee61f9f06c23be2eb0a57f..771e450dc11a6f30e40dc71e7777819d498bae59 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,29 +2,6 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} @layer utilities { .text-balance { @@ -34,18 +11,202 @@ body { /* Customize the scrollbar */ ::-webkit-scrollbar { - width: 6px; /* Set the width of the scrollbar */ + width: 6px; + /* Set the width of the scrollbar */ } ::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.3); /* Color of the thumb */ - border-radius: 3px; /* Rounded corners of the thumb */ + background-color: rgba(0, 0, 0, 0.3); + /* Color of the thumb */ + border-radius: 3px; + /* Rounded corners of the thumb */ } ::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.5); /* Color of the thumb on hover */ + background-color: rgba(0, 0, 0, 0.5); + /* Color of the thumb on hover */ } ::-webkit-scrollbar-track { - background-color: rgba(0, 0, 0, 0.1); /* Color of the track */ + background-color: rgba(0, 0, 0, 0.1); + /* Color of the track */ +} + +/* Tab content - closed */ +.tab-content { + max-height: 0; + -webkit-transition: max-height .35s; + -o-transition: max-height .35s; + transition: max-height .35s; +} + +/* :checked - resize to full height */ +.tab input:checked~.tab-content { + max-height: 100%; +} + +/* Label formatting when open */ +.tab input:checked+label { + /*@apply text-xl p-5 border-l-2 border-indigo-500 bg-gray-100 text-indigo*/ + font-size: 1.25rem; + /*.text-xl*/ + padding: 1.25rem; + /*.p-5*/ + /*border-left-width: 2px; !*.border-l-2*!*/ + /*border-color: #93a84c; !*.border-indigo*!*/ + /*color: #93a84c; !*.text-indigo*!*/ } + +/* Icon */ +.tab label::after { + float: right; + right: 0; + top: 0; + display: block; + width: 1.5em; + height: 1.5em; + line-height: 1.25; + font-size: 1.25rem; + text-align: center; + -webkit-transition: all .35s; + -o-transition: all .35s; + transition: all .35s; +} + +/* Icon formatting - closed */ +.tab input[type=checkbox]+label::after { + content: "+"; + font-weight: bold; + /*.font-bold*/ + border-width: 1px; + /*.border*/ + border-radius: 9999px; + /*.rounded-full */ + border-color: #5c5c5c; + /*.border-grey*/ +} + +.tab input[type=radio]+label::after { + content: "\25BE"; + font-weight: bold; + /*.font-bold*/ + border-width: 1px; + /*.border*/ + border-radius: 9999px; + /*.rounded-full */ + border-color: #5c5c5c; + /*.border-grey*/ +} + +/* Icon formatting - open */ +.tab input[type=checkbox]:checked+label::after { + transform: rotate(315deg); + background-color: #93a84c; + /*.bg-indigo*/ + color: #f8fafc; + /*.text-grey-lightest*/ +} + +.tab input[type=radio]:checked+label::after { + transform: rotateX(180deg); + background-color: #93a84c; + /*.bg-indigo*/ + color: #f8fafc; + /*.text-grey-lightest*/ +} + +.burger.notActive { + height: 4px; + width: 17px; + border-radius: 5px; + background-color: white; + position: relative; + transition: all 0.2s; +} + +.burger.active { + height: 4px; + width: 17px; + border-radius: 5px; + position: relative; + transition: all 0.2s; + opacity: 1; +} + +.burger.active::before { + content: ""; + position: absolute; + top: 8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(-45deg) translate(6px, -6px); + transition: transform 0.2s; +} + +.burger.active::after { + content: ""; + position: absolute; + top: -8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(45deg) translate(5px, 6px); + transition: transform 0.2s; +} + +.burger.notActive::before { + content: ""; + position: absolute; + top: 8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(0) translate(0, 0); + transition: transform 0.2s; +} + +.burger.notActive::after { + content: ""; + position: absolute; + top: -8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(0) translate(0, 0); + transition: transform 0.2s; +} + +#sideBar { + white-space: no-wrap; + height: calc(100dvh - 3.5rem); +} + +@media screen and (max-width:767px) { + #sideBar.active { + width: 100%; + transition-duration: 300ms; + } + + + #sideBar.notActive { + width: 0; + transition-duration: 300ms + } +} + +.tooltip { + @apply invisible absolute; +} + +.has-tooltip:hover .tooltip { + @apply visible z-50; +} \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js index 9aef1df7d6c3d50515ab62f578c5cc963a1243a7..52fe06b6e04cf038208f3a8d0927cf9ceeb9437c 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,17 +1,23 @@ import { Inter } from "next/font/google"; import "./globals.css"; +import Header from "@/app/ui/Header"; +import { NotificationProvider } from "@/context/NotificationContext"; const inter = Inter({ subsets: ["latin"] }); export const metadata = { - title: "Create Next App", + title: "TeamBook", description: "Generated by create next app", }; export default function RootLayout({ children }) { return ( - {children} + + + {children} + + ); } diff --git a/src/app/lib/DateHelper.js b/src/app/lib/DateHelper.js new file mode 100644 index 0000000000000000000000000000000000000000..f4aac7651c2645bb2c14673c40a1bfa538c50039 --- /dev/null +++ b/src/app/lib/DateHelper.js @@ -0,0 +1,32 @@ +export const subtractDays = (date, numberOfDays) => { + const result = new Date(date); + result.setDate(result.getDate() - numberOfDays); + return result; +}; + +/** + * date {Date} + * @return 2024-10-12 + */ +export const extractDate = (date) => { + if (date instanceof Date) + return date.toJSON().split("T")[0] + else throw new Error("date isn't instance of Date in extractDate Util function") +} + + +export const getDateRange = (fromDate, toDate) => { + const startDate = new Date(fromDate); + const endDate = new Date(toDate); + + const dateArray = []; + + let currentDate = startDate; + while (currentDate <= endDate) { + dateArray.push(currentDate.toISOString().split('T')[0].split("-").reverse().join("-")); + + currentDate.setDate(currentDate.getDate() + 1); + } + + return dateArray; +} \ No newline at end of file diff --git a/src/app/lib/StringHelper.js b/src/app/lib/StringHelper.js new file mode 100644 index 0000000000000000000000000000000000000000..fe0d5e25c607e097756639d09800b4a353b8bf9c --- /dev/null +++ b/src/app/lib/StringHelper.js @@ -0,0 +1 @@ +export const removeWhiteSpace = (text) => text.split(" ").filter((element) => element !== "").join("") \ No newline at end of file diff --git a/src/app/lib/TypesHelper.js b/src/app/lib/TypesHelper.js new file mode 100644 index 0000000000000000000000000000000000000000..8077bc8cdb962a8c84c120e94ded330e19f02a38 --- /dev/null +++ b/src/app/lib/TypesHelper.js @@ -0,0 +1,3 @@ +export const isArray = (arg) => { + return Array.isArray(arg) +} \ No newline at end of file diff --git a/src/app/lib/colorsGenerator.js b/src/app/lib/colorsGenerator.js new file mode 100644 index 0000000000000000000000000000000000000000..ba53b5cea8bee55a479ff6657317c444926dad27 --- /dev/null +++ b/src/app/lib/colorsGenerator.js @@ -0,0 +1,5 @@ +import chroma from 'chroma-js'; + +export const generateColors = (numColors) => { + return chroma.scale(["#6574cd", "#f6ad55", "#e53e3e", "#cbd5e0", "#68D391"]).mode('lch').colors(numColors).map(color => ({ backgroundColor: chroma(color).alpha(0.3).css(), borderColor: chroma(color).alpha(0.6).css() })); +}; \ No newline at end of file diff --git a/src/app/lib/constants.js b/src/app/lib/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..9d34296b9316dac3c714bc0476dcfa97e05dda4d --- /dev/null +++ b/src/app/lib/constants.js @@ -0,0 +1,3 @@ +export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; +export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@teamwillgroup\.com$/ +export const PAGINATION_SIZE = 3 \ No newline at end of file diff --git a/src/app/lib/dal.js b/src/app/lib/dal.js deleted file mode 100644 index 74c6293b78f5f50dac5fc7d8a9b11ed8f1b15fbe..0000000000000000000000000000000000000000 --- a/src/app/lib/dal.js +++ /dev/null @@ -1,16 +0,0 @@ -import 'server-only' - -import Cookies from 'js-cookie'; -import { decrypt } from '@/app/lib/session' -import {redirect} from "next/navigation"; - -export const verifySession = cache(async () => { - const cookie = Cookies.get('session').value - const session = await decrypt(cookie) - - if (!session.token) { - redirect('/login') - } - - return { isAuth: true, userName: session.username } -}) \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index 3afb1c3072b662f2ba0461e36390f1f9e0700eed..db2aa42c283fbd6f28b038af117d253871396c2c 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -1,29 +1,42 @@ -//custom fetch tha have the token in header - import Cookies from 'js-cookie'; -import {decrypt} from "@/app/lib/session"; +import { decrypt } from "@/app/lib/session"; +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" const fetchRequest = async (url, options = {}) => { - const token = Cookies.get('session'); - console.log('token', token) - const jwtDecrypted = decrypt(token); - console.log('jwtDecrypted', jwtDecrypted) + const jwtCookie = Cookies.get('session'); + console.log('jwtCookie', jwtCookie) + const jwtDecrypted = await decrypt(jwtCookie); + console.log('jwtDecrypted', jwtDecrypted?.sessionData?.token) const headers = { ...options.headers, - 'Authorization': `Token ${token}`, + // add authorization header with token if there is jwtDecrypted.sessionData.token + Authorization: jwtDecrypted?.sessionData?.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, 'Content-Type': 'application/json', }; - const response = await fetch(url, { + const response = await fetch(`${BASE_URL}${url}`, { ...options, headers, }); - + console.log(response.status) if (!response.ok) { - throw new Error('Network response was not ok'); + try { + const errorData = await response.json(); + return { isSuccess: false, errors: errorData, data: null, status: response.status } + } catch (error) { + return { isSuccess: false, errors: error, data: null, status: response.status } + } } - - return response.json(); + console.log('response', response) + let data = null; + // Check if the response has content before parsing it as JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + data = await response.json(); + return { isSuccess: true, errors: null, data: data, status: response.status }; + } + // If no JSON content, return null for data + return { isSuccess: true, errors: null, data: data, status: response.status }; }; export default fetchRequest; diff --git a/src/app/lib/fetchRequestServer.js b/src/app/lib/fetchRequestServer.js new file mode 100644 index 0000000000000000000000000000000000000000..16aafe94dd8d3c9fe65996005f25770a3231b70e --- /dev/null +++ b/src/app/lib/fetchRequestServer.js @@ -0,0 +1,48 @@ +// src/app/lib/fetchRequestServer.js +import {cookies as nextCookies} from 'next/headers'; +import {decrypt} from "@/app/lib/session"; + +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +const fetchRequestServer = async (url, options = {}) => { + const cookieStore = nextCookies(); + const jwtCookie = cookieStore.get('session')?.value; + + if (!jwtCookie) { + return {isSuccess: false, errors: "No session cookie found", data: null}; + } + + const jwtDecrypted = await decrypt(jwtCookie); + + const headers = { + ...options.headers, + Authorization: jwtDecrypted?.sessionData.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, + 'Content-Type': 'application/json', + }; + + const response = await fetch(`${BASE_URL}${url}`, { + ...options, + headers, + }); + + if (!response.ok) { + try { + const errorData = await response.json(); + return {isSuccess: false, errors: errorData, data: null}; + } catch (error) { + return {isSuccess: false, errors: error, data: null}; + } + } + + // Check if the response has content before parsing it as JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + return {isSuccess: true, errors: null, data: data}; + } + // If no JSON content, return null for data + return {isSuccess: true, errors: null, data: null}; + +}; + +export default fetchRequestServer; diff --git a/src/app/lib/isAuthenticated.js b/src/app/lib/isAuthenticated.js index 0a880acf5d8ce15ae58da3d0295208ff7a4ca3f6..7c77a5e9fe0c6cb8dff56ea1052bd1b27e4b6f6b 100644 --- a/src/app/lib/isAuthenticated.js +++ b/src/app/lib/isAuthenticated.js @@ -1,16 +1,20 @@ //verify if the user is authenticated import Cookies from "js-cookie"; +import {decrypt} from "@/app/lib/session"; -export default function isAuthenticated() { +export default async function isAuthenticated() { if (!Cookies.get('session')) { console.log('user is not authenticated'); // redirect to auth/login page - window.location.href = '/auth/login'; - + // window.location.href = '/auth/login'; return false; } - else console.log('user is authenticated'); - return true; + + console.log('user is authenticated'); + const session = Cookies.get('session') + const cookieDecoded = await decrypt(session) + return { isAuth: true, sessionData: cookieDecoded.sessionData } + } diff --git a/src/app/lib/isAuthenticatedSSR.js b/src/app/lib/isAuthenticatedSSR.js new file mode 100644 index 0000000000000000000000000000000000000000..b06881511928d746e1963011e32bac21034e96b9 --- /dev/null +++ b/src/app/lib/isAuthenticatedSSR.js @@ -0,0 +1,20 @@ +//verify if the user is authenticated +import { cookies } from 'next/headers' + +import {decrypt} from "@/app/lib/session"; + +export default async function isAuthenticatedSSR() { + if (!cookies().get('session')) { + console.log('user is not authenticated'); + // redirect to auth/login page + // window.location.href = '/auth/login'; + return false; + } + + console.log('user is authenticated'); + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + return { isAuth: true, sessionData: session.sessionData } + +} + diff --git a/src/app/lib/session.js b/src/app/lib/session.js index 850ed748db48de63bae014a7fe245ff3a84f3278..1eb509a2e30326ef4e345033f9811c8dc0688513 100644 --- a/src/app/lib/session.js +++ b/src/app/lib/session.js @@ -2,7 +2,8 @@ import Cookies from 'js-cookie'; import { SignJWT, jwtVerify } from 'jose' -const secretKey = "password1234" +const secretKey = process.env.NEXT_PUBLIC_SESSION_SECRET; +console.log(secretKey); const encodedKey = new TextEncoder().encode(secretKey) export async function encrypt(payload) { diff --git a/src/app/no-access/page.jsx b/src/app/no-access/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..62db6653d899d89755ef65932715652413d98c85 --- /dev/null +++ b/src/app/no-access/page.jsx @@ -0,0 +1,45 @@ +"use client"; +import { useRouter } from 'next/navigation'; +import {useEffect, useState} from 'react'; + +export default function NoAccess() { + const router = useRouter(); + const [countdown, setCountdown] = useState(5); + + useEffect(() => { + const interval = setInterval(() => { + // never allow countdown to go below 0 + if (countdown === 0) return; + setCountdown(prevCountdown => prevCountdown - 1); + }, 1000); + + const timeout = setTimeout(() => { + router.push('/'); // Redirect to homepage or a specific page after 5 seconds + }, 5000); + + return () => { + clearInterval(interval); + clearTimeout(timeout); + }; + }, [router]); + + return ( +
+
+

Access Denied

+

+ You do not have the necessary privileges to access this page. +

+

+ You will be redirected in {countdown} seconds... +

+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index ecfca6f8e5034eb7f60a623d3db202433f6b96b9..87ce7aa74a17bf1ba51f0e9979b2d955634bf8ca 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,149 +1,8 @@ -import Image from "next/image"; export default function Home() { return (
-
-

- Get started by editing  - src/app/page.js -

- -
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
+ Home Page
); } diff --git a/src/app/ui/Burger.jsx b/src/app/ui/Burger.jsx new file mode 100644 index 0000000000000000000000000000000000000000..68fafffc6e1e517818b064b228030666f87365b6 --- /dev/null +++ b/src/app/ui/Burger.jsx @@ -0,0 +1,25 @@ +'use client' +import React from 'react' + +const Burger = () => { + const dislaySideBar = (event) => { + const target = event.currentTarget.firstChild + const sideBar = document.getElementById("sideBar") + if (target && sideBar) + if (target.classList.contains("active")) { + target.classList.replace("active", "notActive") + sideBar.classList.replace("active", "notActive") + } + else { + target.classList.replace("notActive", "active") + sideBar.classList.replace("notActive", "active") + } + } + return ( +
+
+
+ ) +} + +export default Burger \ No newline at end of file diff --git a/src/app/ui/ConfirmationModal.jsx b/src/app/ui/ConfirmationModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ad5e6cdd61dac10323164be0121811edb0eb5421 --- /dev/null +++ b/src/app/ui/ConfirmationModal.jsx @@ -0,0 +1,51 @@ +"use client" + +import React from 'react'; + +const ConfirmationModal = ({ isOpen, onClose, onConfirm, message, type }) => { + if (!isOpen) return null; + else if (type === "create") return
+
+

Confirmation

+

{message}

+
+ + +
+
+
+ return ( +
+
+

Confirmation

+

{message}

+
+ + +
+
+
+ ); +}; + +export default ConfirmationModal; diff --git a/src/app/ui/Dropdown.js b/src/app/ui/Dropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..53c7dad6fe0e21fba29535fd2bb25be6263b09af --- /dev/null +++ b/src/app/ui/Dropdown.js @@ -0,0 +1,20 @@ +import React from 'react'; + +const Dropdown = ({ label, options, value, onChange }) => { + return ( +
+ + +
+ ); +}; + +export default Dropdown; diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx new file mode 100644 index 0000000000000000000000000000000000000000..79c3de1582410aa774edf207ff49da647c1f911a --- /dev/null +++ b/src/app/ui/Header.jsx @@ -0,0 +1,52 @@ +// 'use client'; + +import Link from 'next/link'; +import isAuthenticatedSSR from "@/app/lib/isAuthenticatedSSR"; +import LogoutButton from "@/app/ui/LogoutButton"; +import Burger from './Burger'; +import Image from "next/image"; +import logo from "@/static/image/teamwill_logo.png"; + +const Header = async () => { + + const { isAuth, sessionData } = await isAuthenticatedSSR() + console.log('isAuth', isAuth) + console.log('sessionData', sessionData) + return ( +
+
+ + logo +

TeamBook

+ +
+ +
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js new file mode 100644 index 0000000000000000000000000000000000000000..35e621c6f9b3355cbc1f79a37af1862798fd563a --- /dev/null +++ b/src/app/ui/LogoutButton.js @@ -0,0 +1,34 @@ +'use client'; + +import Cookies from 'js-cookie'; +import fetchRequest from "@/app/lib/fetchRequest"; +import LogoutIcon from "@/static/image/svg/logout.svg" + +const LogoutButton = ({ isButton = false }) => { + const logout = async () => { + const response = await fetchRequest(`/logout`, { + method: 'GET' + }); + console.log(response); + console.log('logout successful'); + Cookies.remove('session'); + window.location.href = '/auth/login'; + + }; + if (isButton) + return
+
+

+ Se déconnecter +

+
+
+ return ( + + ); +}; + +export default LogoutButton; diff --git a/src/app/ui/Pagination.jsx b/src/app/ui/Pagination.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6b9d2f027a0844965e5adb624c566acb5997333e --- /dev/null +++ b/src/app/ui/Pagination.jsx @@ -0,0 +1,54 @@ +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" + +const Pagination = ({ paginationData, onPageChange }) => { + if (!paginationData) return null; + + const { currentPage, pagesNumber } = paginationData; + + const handlePageClick = (pageNumber) => { + onPageChange(pageNumber); + }; + + return ( +
+
+ {currentPage > 1 && ( +
handlePageClick(`/projects/pagination/?page=${currentPage - 1}`)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> + +
+ )} + {Array.from({ length: pagesNumber }, (_, index) => index).map((element, index) => { + if (element + 1 === currentPage + 3) { + return ( +

+ ... +

+ ); + } else if (currentPage === element + 1) { + return ( +

+ {element + 1} +

+ ); + } else if (currentPage < element + 3 && element - currentPage < 3) { + return ( +

handlePageClick(`/projects/pagination/?page=${element + 1}`)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center'> + {element + 1} +

+ ); + } else { + return <>; + } + })} + {currentPage !== pagesNumber && ( +
handlePageClick(`/projects/pagination/?page=${currentPage + 1}`)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> + +
+ )} +
+
+ ); +}; + +export default Pagination; \ No newline at end of file diff --git a/src/app/ui/SideBar.css b/src/app/ui/SideBar.css new file mode 100644 index 0000000000000000000000000000000000000000..b471219fe03a550fe2176bbd93a57b638de575c0 --- /dev/null +++ b/src/app/ui/SideBar.css @@ -0,0 +1,47 @@ +/*.relative::before {*/ +/* content: '';*/ +/* position: absolute;*/ +/* top: 50%;*/ +/* left: 0;*/ +/* height: 100%;*/ +/* width: 2px;*/ +/* background-color: #a6b764; !* Tailwind class 'sushi-400' color *!*/ +/* transform: translateY(-50%);*/ +/*}*/ + +/*.relative::after {*/ +/* content: '';*/ +/* position: absolute;*/ +/* top: 50%;*/ +/* left: -1px;*/ +/* width: 8px;*/ +/* height: 8px;*/ +/* background-color: #a6b764; !* Tailwind class 'sushi-400' color *!*/ +/* border-radius: 50%;*/ +/* transform: translate(-50%, -50%);*/ +/*}*/ + +input[type="radio"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid #888888; /* Tailwind class 'sushi-400' color */ + border-radius: 50%; + position: relative; +} + +input[type="radio"]:checked { + border-color: #a6b764; /* Tailwind class 'sushi-400' color */ +} + +input[type="radio"]:checked::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 9px; + height: 9px; + background-color: #a6b764; /* Tailwind class 'sushi-400' color */ + border-radius: 50%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..43288a36c41bdccb4c29c447f982b208248791e0 --- /dev/null +++ b/src/app/ui/SideBar.jsx @@ -0,0 +1,140 @@ +"use client"; +import SideBarLink from "./SideBarLink"; +import RoleIcon from "@/static/image/svg/role.svg" +import UserIcon from "@/static/image/svg/user.svg" +import LogoutButton from "./LogoutButton"; +import isAuthenticated from "@/app/lib/isAuthenticated"; +import {useEffect, useState} from "react"; +import { usePathname } from "next/navigation"; +import './SideBar.css'; + +const SideBar = () => { + const [isAuth, setIsAuth] = useState(false); + const [sessionData, setSessionData] = useState(null); + const pathname = usePathname(); + + useEffect(() => { + const checkAuth = async () => { + const authResult = await isAuthenticated(); + setIsAuth(authResult.isAuth); + setSessionData(authResult.sessionData); + }; + + checkAuth(); + }, []); + + if (!isAuth || !sessionData) { + return ( + + ); + } + const AdminLinks = [ + { label: "Utilisateurs", link: "/user", icon: , privilege: "user" }, + { label: "Habilitations", link: "/privilege", icon: , privilege: "privilege" }, + { label: "Rôles", link: "/role", icon: , privilege: "role" }, + { label: "Projets", link: "/projects", icon: , privilege: "projects" }, + { label: "Réservation", link: "/reservation", icon: , privilege: "reservation" }, + { label: "Type de Presence", link: "/planning/type-presence", icon: , privilege: "planning/type-presence" }, + { label: "Planning", link: "/planning", icon: , privilege: "planning" }, + { label: "Etage", link: "/etage", icon: , privilege: "etage" }, + { label: "Zone", link: "/zone", icon: , privilege: "zone" }, + { label: "Tables", link: "/table", icon: , privilege: "table" }, + { label: "Places", link: "/place", icon: , privilege: "place" }, + { label: "Gestion des zones", link: "/assign-zone-project", icon: , privilege: "assign-zone-project" }, + { label: "Consulter les réservations", link: "/consultation-reservations", icon: , privilege: "consultation-reservations" }, + { label: "Reporting", link: "/reporting", icon: , privilege: "reporting" }, + ]; + + console.log('sessionDataSideBar', sessionData) + const filteredLinks = AdminLinks.filter(link => sessionData?.privileges.includes(link.privilege)); + console.log('filteredLinks', filteredLinks) + + const ConsultationLinks = [ + { + label: "Consulter les réservations", + link: "/consultation-reservations", + icon: + } + ] + + return ( + + ); +} +export default SideBar; \ No newline at end of file diff --git a/src/app/ui/SideBarLink.jsx b/src/app/ui/SideBarLink.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6ae7e4831b95ea65de04e367a0dbd3c7ff3f7d9d --- /dev/null +++ b/src/app/ui/SideBarLink.jsx @@ -0,0 +1,44 @@ +'use client' +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import React from 'react'; + +const SideBarLink = ({ link, label }) => { + const pathname = usePathname(); + + const hideSideBar = () => { + const target = document.querySelector(".burger"); + const sideBar = document.getElementById("sideBar"); + if (target && sideBar) { + target.classList.replace("active", "notActive"); + sideBar.classList.replace("active", "notActive"); + } + console.log(target, sideBar); + }; + + const isActive = pathname === link; + + return ( +
+
+ + + +
+ ); +}; + +export default SideBarLink; diff --git a/src/components/Loader/Loader.css b/src/components/Loader/Loader.css new file mode 100644 index 0000000000000000000000000000000000000000..1e1ca65608312aa3c0c9ecfbed194e5cee0cbf75 --- /dev/null +++ b/src/components/Loader/Loader.css @@ -0,0 +1,46 @@ +@keyframes ldio-gcsicpsikdq { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.ldio-gcsicpsikdq div { + width: 50px; + height: 50px; + border-top-color: transparent; + border: 5px solid transparent; + border-radius: 50%; +} + +.ldio-gcsicpsikdq div { + animation: ldio-gcsicpsikdq 1s linear infinite; +} + +.loadingio-spinner-rolling-daoiuzlm498 { + width: min-content; + height: 70px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: transparent; +} + +.ldio-gcsicpsikdq { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transform: translateZ(0) scale(1); + backface-visibility: hidden; + transform-origin: 0 0; + /* see note above */ +} + +.ldio-gcsicpsikdq div { + box-sizing: content-box; +} \ No newline at end of file diff --git a/src/components/Loader/Loader.jsx b/src/components/Loader/Loader.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2430f19a7bd30aade3569d38228bdc38b43b5288 --- /dev/null +++ b/src/components/Loader/Loader.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import "./Loader.css" +const Loader = ({ size, border, className, color }) => { + return ( +
+
+
+
+
+ ) +} + +export default Loader \ No newline at end of file diff --git a/src/components/Notification/Error.jsx b/src/components/Notification/Error.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6329a934072ae8e2b30be32c6703019794afd7f4 --- /dev/null +++ b/src/components/Notification/Error.jsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from 'react' +import WarningIcon from "@/static/image/svg/WarningIcon.svg" +import CrossIcon from "@/static/image/svg/cross.svg" +import "./Notification.css" + +const Error = ({ displayNotification, message, closeNotification }) => { + const container = useRef(null) + useEffect(() => { + if (!displayNotification) { + container.current.style.transform = "translateX(110%)" + } + }, [displayNotification]) + const hideNotification = () => { + container.current.style.transform = "translateX(110%)" + setTimeout(() => { + closeNotification() + }, 800) + } + return ( +
+
+
+ +
+
+ +
+
+
+

Erreur

+

{message}

+
+
+
+
+
+ ) +} +export default Error \ No newline at end of file diff --git a/src/components/Notification/Notification.css b/src/components/Notification/Notification.css new file mode 100644 index 0000000000000000000000000000000000000000..6f8e680fdc982b32ba35c4305423d202660d0f24 --- /dev/null +++ b/src/components/Notification/Notification.css @@ -0,0 +1,277 @@ +@keyframes timerAnimation { + 0% { + width: 0%; + } + + 100% { + width: 100%; + } +} + +@keyframes notificationPopupContainerAnimation { + 0% { + transform: translateX(110%); + } + + 30% { + transform: translateX(-11%); + } + + 40% { + transform: translateX(-1%); + } + + 45% { + transform: translateX(-8%); + } + + 50% { + transform: translateX(-2%); + } + + 100%, + 60% { + transform: translateX(-5%); + } +} + +.notificationPopup { + z-index: 5000000; + position: fixed; + bottom: 10px; + right: 10px; + transform: translateX(-5%); + transition: 800ms; + animation-name: notificationPopupContainerAnimation; + animation-duration: 1700ms; + background-color: white; + height: 70px; + max-width: 450px; + border-radius: 7px; + width: 83.33%; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-color: #d4d4d4; +} + +@media screen and (min-width: 768px) { + .notificationPopup { + width: 66.66%; + } +} + +@media screen and (min-width: 1024px) { + .notificationPopup { + width: 33.33%; + } +} + +@media screen and (min-width: 1280px) { + .notificationPopup { + width: 30%; + } +} + +.notificationPopup>div { + display: flex; + align-items: center; + height: 100%; + position: relative; +} + +.notificationPopup .part1 { + width: 20%; + display: flex; + align-items: center; + height: 100%; + border-bottom-left-radius: 7px; + border-top-left-radius: 7px; + background-color: rgb(34 197 94); + justify-content: center; +} + +.notificationPopup .part1Warning { + width: 20%; + display: flex; + align-items: center; + height: 100%; + border-bottom-left-radius: 7px; + border-top-left-radius: 7px; + background-color: #facc15; + justify-content: center; +} + +.notificationPopup .part1Error { + width: 20%; + display: flex; + align-items: center; + height: 100%; + border-bottom-left-radius: 7px; + border-top-left-radius: 7px; + background-color: #ef4444; + justify-content: center; +} + +.notificationPopup .part1 .icon { + height: 40px; + width: 40px; + fill: white; + stroke-width: 10px; +} + +.notificationPopup .part1Warning .icon { + height: 40px; + width: 40px; + fill: white; + stroke-width: 10px; +} + +.notificationPopup .part1Error .icon { + height: 40px; + width: 40px; + fill: white; + stroke-width: 10px; +} + +.notificationPopup .part2 { + position: absolute; + z-index: 50; + right: 4px; + top: 4px; + cursor: pointer; +} + +.notificationPopup .part2 .icon { + height: 12px; + width: 12px; + fill: rgb(23 23 23); +} + +.notificationPopup .part3 { + position: relative; + height: 100%; + flex: 1; +} + +.notificationPopup .part3:nth-child(1) { + display: flex; + justify-content: center; + height: 98%; + flex-direction: column; + display: flex; + margin-left: 8px; +} + +.notificationPopup .part3 h2 { + line-height: 20px; + font-size: 18px; + margin-left: 5px; + position: relative; + top: 4px; +} + +.notificationPopup .part3 p { + line-height: 20px; + font-size: 16px; + width: 91.6%; + line-height: 16px; + color: #78716c; + margin-left: 5px; + margin-top: 7px; +} + +.timerSuccess { + position: absolute; + bottom: 0; + height: 4px; + width: 100%; + background-color: transparent; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +.timerWarning { + position: absolute; + bottom: 0; + height: 4px; + width: 100%; + background-color: transparent; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +.timerSuccess::before { + height: 4px; + background-color: rgb(34 197 94); +} + +.timerSuccess::before { + height: 4px; + background-color: rgb(34 197 94); +} + +.timerSuccess::before { + content: ""; + position: absolute; + width: 0%; + animation-name: timerAnimation; + animation-duration: 4000ms; + animation-delay: 1700ms; + animation-timing-function: linear; + left: 0; + top: 0; +} + +.timerWarning::before { + height: 4px; + background-color: #facc15; +} + +.timerWarning::before { + height: 4px; + background-color: #facc15; +} + +.timerWarning::before { + content: ""; + position: absolute; + width: 0%; + animation-name: timerAnimation; + animation-duration: 4000ms; + animation-delay: 1700ms; + animation-timing-function: linear; + left: 0; + top: 0; +} + + +.timerError::before { + height: 4px; + background-color: #ef4444; +} + +.timerError::before { + height: 4px; + background-color: #ef4444; +} + +.timerError::before { + content: ""; + position: absolute; + width: 0%; + animation-name: timerAnimation; + animation-duration: 4000ms; + animation-delay: 1700ms; + animation-timing-function: linear; + left: 0; + top: 0; +} + +.timerError { + position: absolute; + bottom: 0; + height: 4px; + width: 100%; + background-color: transparent; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} \ No newline at end of file diff --git a/src/components/Notification/Notification.jsx b/src/components/Notification/Notification.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4dc60386df416d364c670005ab5fca321f6d45d2 --- /dev/null +++ b/src/components/Notification/Notification.jsx @@ -0,0 +1,66 @@ +import React, { useCallback, useEffect, useState } from 'react' +import Success from './Success' +import Warning from './Warning' +import { useNotification } from '@/context/NotificationContext' +import Error from './Error' +const Notification = () => { + const { toggleNotification, notification } = useNotification() + const [displayNotification, setDisplayNotification] = useState(true) + + var timeOutForwordPropagation + var timeOutBackPropagation + + const closeNotification = useCallback(() => { + clearTimeout(timeOutForwordPropagation) + clearTimeout(timeOutBackPropagation) + toggleNotification({ + visible: false, + type: null, + message: "" + }) + setDisplayNotification(true) + }, [timeOutForwordPropagation, timeOutBackPropagation]) + + + useEffect(() => { + if (notification?.type) { + new Promise((resolve, reject) => { + try { + timeOutForwordPropagation = setTimeout(() => { + setDisplayNotification(false) + resolve() + }, 5700) + } catch (e) { + reject(e) + } + }).then(() => { + timeOutBackPropagation = setTimeout(() => { + toggleNotification({ + visible: false, + type: null, + message: "" + }) + setDisplayNotification(true) + }, 800) + }).catch((error) => { + console.log(error) + }) + + return () => { + clearTimeout(timeOutForwordPropagation) + clearTimeout(timeOutBackPropagation) + } + } + }, [notification?.type]) + + if (notification?.type === "error") + return + if (notification?.type === "success") + return + else if (notification?.type === "warning") + return + else + return
+} + +export default Notification \ No newline at end of file diff --git a/src/components/Notification/Success.jsx b/src/components/Notification/Success.jsx new file mode 100644 index 0000000000000000000000000000000000000000..943570acd9bd6dbab70f4dd9793cb5a0a670f3d9 --- /dev/null +++ b/src/components/Notification/Success.jsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from 'react' +import SuccessIcon from "@/static/image/svg/successIcon.svg" +import CrossIcon from "@/static/image/svg/cross.svg" +import "./Notification.css" + +const Success = ({ displayNotification, message, closeNotification }) => { + const container = useRef(null) + useEffect(() => { + if (!displayNotification) { + container.current.style.transform = "translateX(110%)" + } + }, [displayNotification]) + const hideNotification = () => { + container.current.style.transform = "translateX(110%)" + setTimeout(() => { + closeNotification() + }, 800) + } + return ( +
+
+
+ +
+
+ +
+
+
+

Succès

+

{message}

+
+
+
+
+
+ ) +} +export default Success \ No newline at end of file diff --git a/src/components/Notification/Warning.jsx b/src/components/Notification/Warning.jsx new file mode 100644 index 0000000000000000000000000000000000000000..31eff42865f46e461f28dab4e06a9e6e846b8906 --- /dev/null +++ b/src/components/Notification/Warning.jsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from 'react' +import CrossIcon from "@/static/image/svg/cross.svg" +import WarningIcon from "@/static/image/svg/warningIcon.svg" + +const Warning = ({ displayNotification, message, closeNotification }) => { + const container = useRef(null) + useEffect(() => { + if (!displayNotification) { + container.current.style.transform = "translateX(110%)" + } + }, [displayNotification]) + const hideNotification = () => { + container.current.style.transform = "translateX(110%)" + setTimeout(() => { + closeNotification() + }, 800) + } + return ( +
+
+
+ +
+
+ +
+
+
+

Avertissement

+

{message}

+
+
+
+
+
+ ) +} + +export default Warning \ No newline at end of file diff --git a/src/context/NotificationContext.js b/src/context/NotificationContext.js new file mode 100644 index 0000000000000000000000000000000000000000..9d2bb5c823324be91c19741af43e47b6478d1b40 --- /dev/null +++ b/src/context/NotificationContext.js @@ -0,0 +1,25 @@ +"use client" +import Notification from '@/components/Notification/Notification'; +import React, { createContext, useState, useContext } from 'react'; + +const NotificationContext = createContext(); + +export const useNotification = () => { + return useContext(NotificationContext); +}; + + +export const NotificationProvider = ({ children }) => { + const [notification, setNotification] = useState(null); + + const toggleNotification = (payload) => { + setNotification(payload) + } + + return ( + + {children} + + + ); +}; \ No newline at end of file diff --git a/src/middleware.js b/src/middleware.js index 1308f827736c79a423734acb70d0051515d7d5b1..4b0c14d6ce4510c6cd5bcaf787dc7c1c20408fa7 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,12 +1,10 @@ -"use client"; - import { NextResponse } from 'next/server' import { decrypt } from '@/app/lib/session' -import Cookies from 'js-cookie'; +import { cookies } from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/dashboard'] -const publicRoutes = ['/login', '/signup', '/'] +const protectedRoutes = ['/reporting', '/dashboard', '/auth/verif', '/users', '/privilege', '/projects', '/role', '/etage', '/place', '/zone', '/table', '/reservation', '/planning', '/planning/type-presence', '/assign-zone-project', '/consultation-reservations'] +const publicRoutes = ['/auth/login', '/auth/signup', 'auth/forgot-password', '/auth/change-password'] export default async function middleware(req) { // 2. Check if the current route is protected or public @@ -15,22 +13,33 @@ export default async function middleware(req) { const isPublicRoute = publicRoutes.includes(path) // 3. Decrypt the session from the cookie - const cookie = Cookies.get('session'); - console.log('cookie', cookie) + const cookie = cookies().get('session')?.value const session = await decrypt(cookie) // 5. Redirect to /login if the user is not authenticated - if (isProtectedRoute && !session?.token) { - return NextResponse.redirect(new URL('/login', req.nextUrl)) + if (isProtectedRoute && !session?.sessionData.token) { + return NextResponse.redirect(new URL('/auth/login', req.nextUrl)) + } + + // 6. Check if the user has the necessary privileges + const userPrivileges = session?.sessionData.privileges || [] + console.log('userPrivileges', userPrivileges) + // check if the user the necessary privileges to access the current route + const hasPrivileges = userPrivileges.some(privilege => path.startsWith(`/${privilege}`)); + console.log('hasPrivileges', hasPrivileges) + + if (isProtectedRoute && !hasPrivileges) { + console.log('.') + return NextResponse.redirect(new URL('/no-access', req.nextUrl)); } - // 6. Redirect to /dashboard if the user is authenticated + // 7. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && - session?.token && - !req.nextUrl.pathname.startsWith('/auth') + session?.sessionData.token && + !req.nextUrl.pathname.startsWith('/auth/verif') ) { - return NextResponse.redirect(new URL('/auth', req.nextUrl)) + return NextResponse.redirect(new URL('/auth/verif', req.nextUrl)) } return NextResponse.next() diff --git a/src/static/image/no-data.png b/src/static/image/no-data.png new file mode 100644 index 0000000000000000000000000000000000000000..90f0083b8a12e1f6b08f3dd3c2e301a81562afd4 Binary files /dev/null and b/src/static/image/no-data.png differ diff --git a/src/static/image/svg/add.svg b/src/static/image/svg/add.svg new file mode 100644 index 0000000000000000000000000000000000000000..37eaf783c6b70ff9297a84dfff632105ad288efe --- /dev/null +++ b/src/static/image/svg/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/cancel.svg b/src/static/image/svg/cancel.svg new file mode 100644 index 0000000000000000000000000000000000000000..1a52a76615e2ee52c46822777d48a7b04b1f8113 --- /dev/null +++ b/src/static/image/svg/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/check.svg b/src/static/image/svg/check.svg new file mode 100644 index 0000000000000000000000000000000000000000..f169344e257f6f1c7aacb4d33fc3fb1f790d3119 --- /dev/null +++ b/src/static/image/svg/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/chevron-left.svg b/src/static/image/svg/chevron-left.svg new file mode 100644 index 0000000000000000000000000000000000000000..bfbeddaec4e6e4a0ae5f8210efcc1f2044fca927 --- /dev/null +++ b/src/static/image/svg/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/chevron-right.svg b/src/static/image/svg/chevron-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..49c8a53fcd0352ccbec6886ee77927dd10244b18 --- /dev/null +++ b/src/static/image/svg/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/cross.svg b/src/static/image/svg/cross.svg new file mode 100644 index 0000000000000000000000000000000000000000..f0c90d5047732e14d0693dfab195fb9d352cf075 --- /dev/null +++ b/src/static/image/svg/cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/delete.svg b/src/static/image/svg/delete.svg new file mode 100644 index 0000000000000000000000000000000000000000..37a3374c3a00ed9249b891340e143a0304ef189f --- /dev/null +++ b/src/static/image/svg/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/desk-1.svg b/src/static/image/svg/desk-1.svg new file mode 100644 index 0000000000000000000000000000000000000000..3bfcdbd40eb521168a533367e39d0c2a444c6aea --- /dev/null +++ b/src/static/image/svg/desk-1.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/static/image/svg/desk.svg b/src/static/image/svg/desk.svg new file mode 100644 index 0000000000000000000000000000000000000000..9c413fe224320ed9ba78ee0dd6d5d8aee2acfa5f --- /dev/null +++ b/src/static/image/svg/desk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/edit.svg b/src/static/image/svg/edit.svg new file mode 100644 index 0000000000000000000000000000000000000000..9f6730fd01232372fc31d62cf49a68627c09ccb3 --- /dev/null +++ b/src/static/image/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/logout.svg b/src/static/image/svg/logout.svg new file mode 100644 index 0000000000000000000000000000000000000000..932b461902b4b748292a2eea4dcbb18a26c78e0b --- /dev/null +++ b/src/static/image/svg/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/no-data-ill.svg b/src/static/image/svg/no-data-ill.svg new file mode 100644 index 0000000000000000000000000000000000000000..992a01c698d067a3ce2015abceccb1c728b3bfaa --- /dev/null +++ b/src/static/image/svg/no-data-ill.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/place.svg b/src/static/image/svg/place.svg new file mode 100644 index 0000000000000000000000000000000000000000..cd980fb5ad668846821b605178091cf7f442719e --- /dev/null +++ b/src/static/image/svg/place.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/project.svg b/src/static/image/svg/project.svg new file mode 100644 index 0000000000000000000000000000000000000000..db8fcbc34971ee8ea812122b4b013011077642c7 --- /dev/null +++ b/src/static/image/svg/project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/role.svg b/src/static/image/svg/role.svg new file mode 100644 index 0000000000000000000000000000000000000000..d8f13942ea7db906be026abcc4c5d7cf50b481ca --- /dev/null +++ b/src/static/image/svg/role.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/simple-table.svg b/src/static/image/svg/simple-table.svg new file mode 100644 index 0000000000000000000000000000000000000000..178f298407e0fe124529c42a9332f8c95c84e0b3 --- /dev/null +++ b/src/static/image/svg/simple-table.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/static/image/svg/statistics.svg b/src/static/image/svg/statistics.svg new file mode 100644 index 0000000000000000000000000000000000000000..a84c1659d9f3f54f3ae8b0af0247830193ddb0dd --- /dev/null +++ b/src/static/image/svg/statistics.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/static/image/svg/study-desk.svg b/src/static/image/svg/study-desk.svg new file mode 100644 index 0000000000000000000000000000000000000000..7b2fcbedc7de74fe5b5e9cdaa4293325512f7ff2 --- /dev/null +++ b/src/static/image/svg/study-desk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/successIcon.svg b/src/static/image/svg/successIcon.svg new file mode 100644 index 0000000000000000000000000000000000000000..57199b67460a6e381793e902fa51c699449090b1 --- /dev/null +++ b/src/static/image/svg/successIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/table.svg b/src/static/image/svg/table.svg new file mode 100644 index 0000000000000000000000000000000000000000..0d56955abbb30e580bf87d3f9e05f09d93691805 --- /dev/null +++ b/src/static/image/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/user.svg b/src/static/image/svg/user.svg new file mode 100644 index 0000000000000000000000000000000000000000..e501f458077e266a936e8adf199ed99e2dbe70a6 --- /dev/null +++ b/src/static/image/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/warningIcon.svg b/src/static/image/svg/warningIcon.svg new file mode 100644 index 0000000000000000000000000000000000000000..8fdac5d1bf288ebf1fb6cb63992ec1488da89d64 --- /dev/null +++ b/src/static/image/svg/warningIcon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/static/image/teamwill_logo.png b/src/static/image/teamwill_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8416c0d9981aa9389d90967a30f62060d76d1d57 Binary files /dev/null and b/src/static/image/teamwill_logo.png differ diff --git a/tailwind.config.js b/tailwind.config.js index bb1243c2439aa40da55d352ec149d19dba4b43b2..910f62575320a3695ea9880b0fd217be746c3694 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -28,7 +28,7 @@ module.exports = { '300': '#b0b0b0', '400': '#888888', '500': '#6d6d6d', - '600': '#5c5c5c', // grayTW: "#5c5c5c", + '600': '#5c5c5c', // grayTW: "#5c5c5c", '700': '#4f4f4f', '800': '#454545', '900': '#3d3d3d',