diff --git a/.gitignore b/.gitignore index dadd40864..ade10337b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,12 @@ local.log log/ results/ +# cypress +browserstack.json +local.log +log/ +results/ + npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index 2641a0457..bf24530b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,173 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## 2.14.0 (2024-01-26) + + +### Features + +* show group creator and date ([3f27e0d](https://github.com/onlineberatung/onlineberatung-frontend/commit/3f27e0d60227c94853457e7778537f18be3e6c62)) + +### 2.13.31 (2024-01-26) + + +### Bug Fixes + +* updated review comments ([b90b0a1](https://github.com/onlineberatung/onlineberatung-frontend/commit/b90b0a1ed92ea09cc6a16b586c72f1a8c0e703e7)) + +### 2.13.30 (2024-01-17) + +### 2.13.29 (2024-01-16) + +### 2.13.28 (2024-01-16) + +### 2.13.27 (2024-01-16) + +### 2.13.26 (2024-01-16) + +### 2.13.25 (2024-01-10) + +### 2.13.24 (2023-11-29) + +### 2.13.23 (2023-11-29) + +### 2.13.22 (2023-11-23) + +### 2.13.21 (2023-10-04) + +### 2.13.20 (2023-09-27) + +### 2.13.19 (2023-09-25) + +### 2.13.18 (2023-09-25) + +### 2.13.17 (2023-09-12) + +### 2.13.16 (2023-09-11) + +### 2.13.15 (2023-09-11) + +### 2.13.14 (2023-09-11) + +### 2.13.13 (2023-08-21) + +### 2.13.12 (2023-08-16) + +### 2.13.11 (2023-08-10) + +### 2.13.10 (2023-08-09) + +### 2.13.9 (2023-08-08) + +### 2.13.8 (2023-08-07) + +### 2.13.7 (2023-08-07) + +### 2.13.6 (2023-07-26) + +### 2.13.5 (2023-07-26) + +### 2.13.4 (2023-07-13) + +### 2.13.3 (2023-07-12) + + +### Bug Fixes + +* **ban user:** dont close overlay on state change ([01dc7a3](https://github.com/onlineberatung/onlineberatung-frontend/commit/01dc7a349ad6a8a7d41acd28b4dc633f54dcf662)) + +### 2.13.2 (2023-06-27) + + +### Bug Fixes + +* change to use agency instead of consultant ([a287580](https://github.com/onlineberatung/onlineberatung-frontend/commit/a287580425bc7bcbadd91e5e9a12dd30a3c7a22e)) + +### 2.13.1 (2023-06-26) + + +### Bug Fixes + +* when group chat is first position OB-5233 ([b8fb517](https://github.com/onlineberatung/onlineberatung-frontend/commit/b8fb51743f033719aea3bd8eaf00fad862e341bf)) + +## 2.13.0 (2023-06-22) + + +### Features + +* adding the OB-5223 ([eda96f2](https://github.com/onlineberatung/onlineberatung-frontend/commit/eda96f2906e46b639d27774e1419de08f9607a4a)) + +### 2.12.2 (2023-06-21) + + +### Bug Fixes + +* adding the digital and live OB-5221 ([8f9376a](https://github.com/onlineberatung/onlineberatung-frontend/commit/8f9376aa958d5b0fd5e665cbd2a69b4eb8f00bc6)) + +### 2.12.1 (2023-06-21) + + +### Bug Fixes + +* remove leave chat if user is banned OB-5219 ([9c6d9e2](https://github.com/onlineberatung/onlineberatung-frontend/commit/9c6d9e2e494f88a942859d19134f5cb9a192dc59)) + +## 2.12.0 (2023-06-21) + + +### Features + +* remove unused code ([f73c16c](https://github.com/onlineberatung/onlineberatung-frontend/commit/f73c16cc38d5475ec48db269691af4ee5c989a8e)) + +### 2.11.1 (2023-05-22) + + +### Bug Fixes + +* another typos OB-4989, OB-4869 ([682e2ee](https://github.com/onlineberatung/onlineberatung-frontend/commit/682e2ee4b4449d41435252318b885e1f19d6da40)) + +## 2.11.0 (2023-05-22) + + +### Features + +* adding the new descriptions OB-4989 and new translations provided by Niklas ([a808ec4](https://github.com/onlineberatung/onlineberatung-frontend/commit/a808ec4430eebdb9adca2c647841e7728f18d174)) + +## 2.10.0 (2023-05-19) + + +### Features + +* adding the informal language for terms and conditions OB-4869 ([f7215d9](https://github.com/onlineberatung/onlineberatung-frontend/commit/f7215d94772b9cc66dad4d31a4f8ab6a0565389a)) + +### 2.9.21 (2023-05-17) + + +### Bug Fixes + +* revert team beratung descriptions OB-4646 ([bc11377](https://github.com/onlineberatung/onlineberatung-frontend/commit/bc11377a521edd7287b2231121c821519133cbf3)) + +### 2.9.20 (2023-05-16) + + +### Bug Fixes + +* overview available to AS OB-4851 ([15f97f1](https://github.com/onlineberatung/onlineberatung-frontend/commit/15f97f1c972ee26b70ac13a1561078e986ea55b6)) + +### 2.9.19 (2023-05-05) + +### 2.9.18 (2023-05-04) + +### 2.9.17 (2023-05-03) + +### 2.9.16 (2023-04-27) + +### 2.9.15 (2023-01-04) + + +### Bug Fixes + +* issue when changing the translation ([f75dfd6](https://github.com/onlineberatung/onlineberatung-frontend/commit/f75dfd626af738e94d2bb1ef532a95b5c2f1baf8)) + ### 2.9.14 (2022-03-08) ### 2.9.13 (2022-02-28) diff --git a/config/webpack.config.js b/config/webpack.config.js index a1610b2e3..64e969541 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -91,26 +91,6 @@ const getTemplate = (templatePath) => { return path.resolve(paths.appSrc, templatePath); }; -const localAliases = (paths) => - paths - // Remove paths which are not overridden - .filter((localPath) => { - const fullPath = path.resolve(process.cwd(), `./${localPath}`); - try { - fs.statSync(fullPath); - return true; - } catch (error) { - return false; - } - }) - .map( - (localPath) => - new webpack.NormalModuleReplacementPlugin( - new RegExp(localPath), - path.resolve(process.cwd(), `./${localPath}`) - ) - ); - // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. module.exports = function (webpackEnv) { @@ -845,34 +825,51 @@ module.exports = function (webpackEnv) { fs.existsSync(`${newPath}${originalExt}`) && !fs.lstatSync(`${newPath}${originalExt}`).isDirectory() ) { - console.log( - `Overwritten ${originalPath} -> ${newPath}${originalExt}` - ); + // Skip override logic if issuer is itself + if ( + result.contextInfo.issuer === + `${newPath}${originalExt}` + ) { + console.log( + `Self reference ${originalPath} -> ${newPath}${originalExt}` + ); + return; + } if (result.createData) { - result.createData.resource = `${newPath}${originalExt}`; - result.createData.context = path.dirname( - `${newPath}.${originalExt}` + console.log( + `Overwritten ${originalPath} -> ${newPath}${originalExt}` + ); + result.request = result.request.replace( + originalPath, + `${newPath}${originalExt}` + ); + result.context = path.dirname( + `${newPath}${originalExt}` + ); + if (result.createData.request) { + result.createData.resource = + result.createData.resource.replace( + paths.appSrc, + paths.appExtensions + ); + result.createData.request = + result.createData.request.replace( + originalPath, + `${newPath}` + ); + result.createData.context = path.dirname( + `${newPath}${originalExt}` + ); + } + } else { + console.log( + `No createData ${originalPath} -> ${newPath}${originalExt}` ); } } } - ), - ...localAliases([ - 'src/resources/img/illustrations/answer.svg', - 'src/resources/img/illustrations/arrow.svg', - 'src/resources/img/illustrations/bad-request.svg', - 'src/resources/img/illustrations/check.svg', - 'src/resources/img/illustrations/consultant.svg', - 'src/resources/img/illustrations/envelope-check.svg', - 'src/resources/img/illustrations/internal-server-error.svg', - 'src/resources/img/illustrations/not-found.svg', - 'src/resources/img/illustrations/unauthorized.svg', - 'src/resources/img/illustrations/waiting.svg', - 'src/resources/img/illustrations/waving.svg', - 'src/resources/img/illustrations/welcome.svg', - 'src/resources/img/illustrations/x.svg' - ]) + ) ].filter(Boolean), // Turn off performance processing because we utilize // our own hints via the FileSizeReporter diff --git a/cypress/e2e/registration.cy.ts b/cypress/e2e/registration.cy.ts index 6ee145ca0..77ec0afd7 100644 --- a/cypress/e2e/registration.cy.ts +++ b/cypress/e2e/registration.cy.ts @@ -43,6 +43,33 @@ describe('registration', () => { }); describe('addiction', () => { + beforeEach(() => { + cy.intercept(endpoints.topicsData, [ + { + id: 1, + name: 'Alkohol' + } + ]); + }); + + it('should have all generic registration page elements', () => { + cy.visit('/suchtberatung/registration'); + cy.wait('@consultingTypeServiceBySlugFull'); + cy.get('[data-cy="close-welcome-screen"]').click(); + + cy.get('.registrationFormDigi__Input').should('exist'); + cy.get('input[name="gender"]').should('exist'); + cy.get('input[name="counsellingRelation"]').should('exist'); + cy.get( + '.registrationFormDigi__InputTopicIdsContainer input' + ).should('exist'); + cy.get('#username').should('exist'); + cy.get('#passwordInput').should('exist'); + cy.get('#passwordConfirmation').should('exist'); + cy.get('.button__primary').should('exist'); + cy.get('.stageLayout__toLogin').should('exist'); + }); + it('should have all generic registration page elements', () => { cy.visit('/suchtberatung/registration'); cy.wait('@consultingTypeServiceBySlugFull'); diff --git a/package-lock.json b/package-lock.json index ef84d0083..af82bf73d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@onlineberatung/onlineberatung-frontend", - "version": "2.9.14", + "version": "2.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@onlineberatung/onlineberatung-frontend", - "version": "2.9.14", + "version": "2.14.0", "dependencies": { "@babel/core": "^7.23.7", "@babel/eslint-parser": "^7.23.3", @@ -42,6 +42,7 @@ "bytebuffer": "^5.0.1", "caniuse-lite": "^1.0.30001579", "case-sensitive-paths-webpack-plugin": "^2.4.0", + "classnames": "^2.5.1", "clsx": "^1.1.1", "copy-webpack-plugin": "^12.0.2", "core-js": "^3.35.1", @@ -73,6 +74,7 @@ "fs-extra": "^11.2.0", "get-contrast": "^3.0.0", "hi-base32": "0.5.1", + "html-react-parser": "^1.4.14", "html-webpack-plugin": "^5.6.0", "i18next": "^21.8.16", "i18next-browser-languagedetector": "^6.1.5", @@ -97,6 +99,7 @@ "postcss-safe-parser": "^6.0.0", "prompts": "^2.4.2", "qrcode": "^1.5.0", + "rc-field-form": "^1.27.1", "react": "^17.0.2", "react-app-polyfill": "^3.0.0", "react-csv": "^2.2.2", @@ -5525,6 +5528,11 @@ "version": "3.2.5", "license": "MIT" }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, "node_modules/asynciterator.prototype": { "version": "1.0.0", "license": "MIT", @@ -6499,7 +6507,8 @@ }, "node_modules/classnames": { "version": "2.5.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "node_modules/clean-css": { "version": "5.3.3", @@ -12997,6 +13006,92 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-dom-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.2.0.tgz", + "integrity": "sha512-2HIpFMvvffsXHFUFjso0M9LqM+1Lm22BF+Df2ba+7QHJXjk63pWChEnI6YG27eaWqUdfnh5/Vy+OXrNTtepRsg==", + "dependencies": { + "domhandler": "4.3.1", + "htmlparser2": "7.2.0" + } + }, + "node_modules/html-dom-parser/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, "node_modules/html-entities": { "version": "2.4.0", "funding": [ @@ -13044,6 +13139,34 @@ "void-elements": "3.1.0" } }, + "node_modules/html-react-parser": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.14.tgz", + "integrity": "sha512-pxhNWGie8Y+DGDpSh8cTa0k3g8PsDcwlfolA+XxYo1AGDeB6e2rdlyv4ptU9bOTiZ2i3fID+6kyqs86MN0FYZQ==", + "dependencies": { + "domhandler": "4.3.1", + "html-dom-parser": "1.2.0", + "react-property": "2.0.0", + "style-to-js": "1.1.1" + }, + "peerDependencies": { + "react": "0.14 || 15 || 16 || 17 || 18" + } + }, + "node_modules/html-react-parser/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/html-tags": { "version": "3.3.1", "dev": true, @@ -13424,6 +13547,11 @@ "node": ">=10" } }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/inquirer": { "version": "8.2.5", "dev": true, @@ -18425,6 +18553,36 @@ "rc": "cli.js" } }, + "node_modules/rc-field-form": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.41.0.tgz", + "integrity": "sha512-k9AS0wmxfJfusWDP/YXWTpteDNaQ4isJx9UKxx4/e8Dub4spFeZ54/EuN2sYrMRID/+hUznPgVZeg+Gf7XSYCw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "async-validator": "^4.1.0", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.1.tgz", + "integrity": "sha512-e4ZMs7q9XqwTuhIK7zBIVFltUtMSjphuPPQXHoHlzRzNdOwUxDejo0Zls5HYaJfRKNURcsS/ceKVULlhjBrxng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", "dev": true, @@ -18739,6 +18897,11 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", + "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" + }, "node_modules/react-refresh": { "version": "0.14.0", "license": "MIT", @@ -20733,6 +20896,22 @@ "dev": true, "license": "ISC" }, + "node_modules/style-to-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.1.tgz", + "integrity": "sha512-RJ18Z9t2B02sYhZtfWKQq5uplVctgvjTfLWT7+Eb1zjUjIrWzX5SdlkwLGQozrqarTmEzJJ/YmdNJCUNI47elg==", + "dependencies": { + "style-to-object": "0.3.0" + } + }, + "node_modules/style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/stylehacks": { "version": "6.0.2", "license": "MIT", diff --git a/package.json b/package.json index 73c3add25..b6157c574 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@onlineberatung/onlineberatung-frontend", "title": "Online-Beratung", - "version": "2.9.14", + "version": "2.14.0", "repository": { "type": "git", "url": "https://github.com/onlineberatung/onlineberatung-frontend.git" @@ -46,6 +46,7 @@ "bytebuffer": "^5.0.1", "caniuse-lite": "^1.0.30001579", "case-sensitive-paths-webpack-plugin": "^2.4.0", + "classnames": "^2.5.1", "clsx": "^1.1.1", "copy-webpack-plugin": "^12.0.2", "core-js": "^3.35.1", @@ -77,6 +78,7 @@ "fs-extra": "^11.2.0", "get-contrast": "^3.0.0", "hi-base32": "0.5.1", + "html-react-parser": "^1.4.14", "html-webpack-plugin": "^5.6.0", "i18next": "^21.8.16", "i18next-browser-languagedetector": "^6.1.5", @@ -101,6 +103,7 @@ "postcss-safe-parser": "^6.0.0", "prompts": "^2.4.2", "qrcode": "^1.5.0", + "rc-field-form": "^1.27.1", "react": "^17.0.2", "react-app-polyfill": "^3.0.0", "react-csv": "^2.2.2", @@ -170,7 +173,7 @@ "scripts": { "start": "node proxy/server.js", "dev": "node scripts/start.js", - "dev:server": "nodemon --watch scripts --watch config --watch src/setupProxy.js --watch proxy --exec \"npm run dev\"", + "dev:server": "nodemon", "build": "node scripts/build.js", "test": "cross-env BROWSER=none REACT_APP_API_URL=http://127.0.0.1:9001 HTTPS=0 PORT=9001 WDS_SOCKET_PORT=9001 concurrently --kill-others --success first \"npm run dev\" \"wait-on http://127.0.0.1:9001 && cypress run\"", "test:build": "cross-env BROWSER=none PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 concurrently --kill-others --success first \"npm run start\" \"wait-on http://127.0.0.1:9001 && cypress run\"", @@ -251,5 +254,14 @@ "extends": [ "@commitlint/config-conventional" ] + }, + "nodemonConfig": { + "exec": "npm run dev", + "watch": [ + "scripts/", + "config/", + "proxy/" + ], + "delay": 2500 } } diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 416b50eed..000000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 000000000..021180f06 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 000000000..aaf4efbce Binary files /dev/null and b/public/logo.png differ diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index 23a9a1fea..000000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index 1413c0f25..000000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 139e5c67e..000000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "Online-Beratung", - "name": "Caritas Online-Beratung – Online. Anonym. Sicher.", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#cc1e1c", - "background_color": "#ffffff" -} diff --git a/src/api/apiAgencySelection.ts b/src/api/apiAgencySelection.ts index 18eb3cfb4..a9c9329d0 100644 --- a/src/api/apiAgencySelection.ts +++ b/src/api/apiAgencySelection.ts @@ -8,6 +8,9 @@ export const apiAgencySelection = async ( postcode: string; consultingType: number | undefined; topicId?: number; + age?: number; + gender?: string; + counsellingRelation?: string; }, signal?: AbortSignal ): Promise | null> => { diff --git a/src/api/apiGetUserDataBySessionId.ts b/src/api/apiGetUserDataBySessionId.ts index 3bc849f73..58c716b09 100644 --- a/src/api/apiGetUserDataBySessionId.ts +++ b/src/api/apiGetUserDataBySessionId.ts @@ -1,12 +1,14 @@ import { endpoints } from '../resources/scripts/endpoints'; -import { fetchData, FETCH_METHODS } from './fetchData'; +import { fetchData, FETCH_METHODS, FETCH_ERRORS } from './fetchData'; +import { ConsultingSessionDataInterface } from '../globalState'; export const apiGetUserDataBySessionId = async ( sessionId: number -): Promise => { +): Promise => { return fetchData({ url: endpoints.userDataBySessionId(sessionId), rcValidation: true, - method: FETCH_METHODS.GET + method: FETCH_METHODS.GET, + responseHandling: [FETCH_ERRORS.FORBIDDEN] }); }; diff --git a/src/api/index.ts b/src/api/index.ts index 44d6af074..0bbb47cae 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,7 +16,6 @@ export * from './apiGetAppointmentServiceTeam'; export * from './apiGetApiAppointmentServiceEventTypes'; export * from './apiGetAppointmentsServiceBookingEventsByUserId'; export * from './apiGetGroupChatInfo'; -export * from './apiGetGroupMembers'; export * from './apiGetSessionData'; export * from './apiGetUserData'; export * from './apiGroupChatSettings'; diff --git a/src/components/agencySelection/AgencySelection.tsx b/src/components/agencySelection/AgencySelection.tsx index 60f85e09c..475326872 100644 --- a/src/components/agencySelection/AgencySelection.tsx +++ b/src/components/agencySelection/AgencySelection.tsx @@ -41,6 +41,8 @@ export interface AgencySelectionProps { initialPostcode?: string; hideExternalAgencies?: boolean; onKeyDown?: Function; + age?: number; + gender?: string; } export const AgencySelection = (props: AgencySelectionProps) => { @@ -86,7 +88,9 @@ export const AgencySelection = (props: AgencySelectionProps) => { const response = await apiAgencySelection({ postcode: DEFAULT_POSTCODE, consultingType: props.consultingType.id, - topicId: props?.mainTopicId + topicId: props?.mainTopicId, + age: props?.age, + gender: props?.gender }); const defaultAgency = response[0]; @@ -101,7 +105,14 @@ export const AgencySelection = (props: AgencySelectionProps) => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [autoSelectAgency, props.consultingType.id, props?.mainTopicId, locale]); + }, [ + autoSelectAgency, + props.consultingType.id, + props?.mainTopicId, + props?.age, + props?.gender, + locale + ]); useEffect(() => { if (isSelectedAgencyValidated()) { @@ -150,7 +161,9 @@ export const AgencySelection = (props: AgencySelectionProps) => { await apiAgencySelection({ postcode: selectedPostcode, consultingType: props.consultingType.id, - topicId: props?.mainTopicId + topicId: props?.mainTopicId, + age: props?.age, + gender: props?.gender }).finally(() => setIsLoading(false)) ).filter( (agency) => @@ -163,23 +176,21 @@ export const AgencySelection = (props: AgencySelectionProps) => { setProposedAgencies(null); } } catch (err: any) { - if ( - err.message === FETCH_ERRORS.EMPTY && - props.consultingType.id !== null - ) { + if (err.message === FETCH_ERRORS.EMPTY) { setProposedAgencies(null); - setPostcodeFallbackLink( - parsePlaceholderString( - settings.postcodeFallbackUrl, - { - url: props.consultingType.urls - .registrationPostcodeFallbackUrl, - postcode: selectedPostcode - } - ) - ); + if (props.consultingType.id !== null) { + setPostcodeFallbackLink( + parsePlaceholderString( + settings.postcodeFallbackUrl, + { + url: props.consultingType.urls + .registrationPostcodeFallbackUrl, + postcode: selectedPostcode + } + ) + ); + } } - return; } })(); } else if ( @@ -194,7 +205,13 @@ export const AgencySelection = (props: AgencySelectionProps) => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedPostcode, props.consultingType.id, props?.mainTopicId]); + }, [ + selectedPostcode, + props.consultingType.id, + props?.mainTopicId, + props?.age, + props?.gender + ]); const postcodeInputItem: InputFieldItem = { name: 'postcode', diff --git a/src/components/app/RocketChat.ts b/src/components/app/RocketChat.ts index e06690b53..fe778cf12 100644 --- a/src/components/app/RocketChat.ts +++ b/src/components/app/RocketChat.ts @@ -43,6 +43,7 @@ type filter = { export type UserResponse = { name: string | null; + displayName: string | null; status: Status; username: string; _id: string; diff --git a/src/components/askerInfo/AskerInfo.tsx b/src/components/askerInfo/AskerInfo.tsx index 56ac8eaad..22bcb7e10 100644 --- a/src/components/askerInfo/AskerInfo.tsx +++ b/src/components/askerInfo/AskerInfo.tsx @@ -1,23 +1,11 @@ import * as React from 'react'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useContext, useEffect } from 'react'; import { Link, useParams, useHistory } from 'react-router-dom'; -import { - SESSION_LIST_TAB, - SESSION_LIST_TYPES -} from '../session/sessionHelpers'; -import { - AUTHORITIES, - hasUserAuthority, - SessionTypeContext, - TenantContext, - UserDataContext -} from '../../globalState'; +import { SESSION_LIST_TAB } from '../session/sessionHelpers'; +import { SessionTypeContext } from '../../globalState'; import { Loading } from '../app/Loading'; -import { AskerInfoData } from './AskerInfoData'; import { ReactComponent as BackIcon } from '../../resources/img/icons/arrow-left.svg'; import { ReactComponent as PersonIcon } from '../../resources/img/icons/person.svg'; -import { AskerInfoAssign } from './AskerInfoAssign'; -import '../profile/profile.styles'; import './askerInfo.styles'; import { ActiveSessionContext } from '../../globalState/provider/ActiveSessionProvider'; import { useSearchParam } from '../../hooks/useSearchParams'; @@ -29,38 +17,29 @@ import { mobileUserProfileView } from '../app/navigationHandler'; import { useTranslation } from 'react-i18next'; -import { AskerInfoTools } from './AskerInfoTools'; -import { Box } from '../box/Box'; import { RocketChatUsersOfRoomProvider } from '../../globalState/provider/RocketChatUsersOfRoomProvider'; +import { AskerInfoContent } from './AskerInfoContent'; export const AskerInfo = () => { const { t: translate } = useTranslation(); - const { tenant } = useContext(TenantContext); const { rcGroupId: groupIdFromParam } = useParams<{ rcGroupId: string }>(); const history = useHistory(); - const { userData } = useContext(UserDataContext); - const { type, path: listPath } = useContext(SessionTypeContext); + const { path: listPath } = useContext(SessionTypeContext); const { session: activeSession, ready } = useSession(groupIdFromParam); - const [isPeerChat, setIsPeerChat] = useState(false); const sessionListTab = useSearchParam('sessionListTab'); useEffect(() => { - if (!ready) { + if (!ready || activeSession) { return; } - if (!activeSession) { - history.push( - listPath + - (sessionListTab ? `?sessionListTab=${sessionListTab}` : '') - ); - return; - } - - setIsPeerChat(activeSession.item.isPeerChat); + history.push( + listPath + + (sessionListTab ? `?sessionListTab=${sessionListTab}` : '') + ); }, [activeSession, history, listPath, ready, sessionListTab]); const { fromL } = useResponsive(); @@ -74,33 +53,8 @@ export const AskerInfo = () => { desktopView(); }, [fromL]); - const isSessionAssignAvailable = useCallback( - () => - !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && - !activeSession.isLive && - !activeSession.isGroup && - ((type === SESSION_LIST_TYPES.ENQUIRY && - hasUserAuthority( - AUTHORITIES.ASSIGN_CONSULTANT_TO_ENQUIRY, - userData - ) && - isPeerChat) || - (type !== SESSION_LIST_TYPES.ENQUIRY && - ((isPeerChat && - hasUserAuthority( - AUTHORITIES.ASSIGN_CONSULTANT_TO_PEER_SESSION, - userData - )) || - (!isPeerChat && - hasUserAuthority( - AUTHORITIES.ASSIGN_CONSULTANT_TO_SESSION, - userData - ))))), - [activeSession, isPeerChat, type, userData] - ); - if (!activeSession) { - return ; + return ; } return ( @@ -150,21 +104,7 @@ export const AskerInfo = () => {

{activeSession.user.username}

- - - - {tenant?.settings?.featureToolsEnabled && ( - - - - )} - {isSessionAssignAvailable() && ( - -
- -
-
- )} +
diff --git a/src/components/askerInfo/AskerInfoAssign.tsx b/src/components/askerInfo/AskerInfoAssign.tsx index 1a245bfb4..5bc67d3ff 100644 --- a/src/components/askerInfo/AskerInfoAssign.tsx +++ b/src/components/askerInfo/AskerInfoAssign.tsx @@ -10,7 +10,11 @@ import { Text } from '../text/Text'; import { ActiveSessionContext } from '../../globalState/provider/ActiveSessionProvider'; import { useTranslation } from 'react-i18next'; -export const AskerInfoAssign = () => { +export const AskerInfoAssign = ({ + title = 'userProfile.reassign.title' +}: { + title?: string | null; +}) => { const { t: translate } = useTranslation(); const { activeSession } = useContext(ActiveSessionContext); const { userData } = useContext(UserDataContext); @@ -19,10 +23,7 @@ export const AskerInfoAssign = () => { !activeSession.isLive && hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData) && ( <> - + { + const { tenant } = useContext(TenantContext); + const { activeSession } = useContext(ActiveSessionContext); + const { userData } = useContext(UserDataContext); + + const { type } = useContext(SessionTypeContext); + + const isSessionAssignAvailable = useCallback(() => { + const isPeerChat = activeSession.item.isPeerChat; + return ( + !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && + !activeSession.isLive && + !activeSession.isGroup && + ((type === SESSION_LIST_TYPES.ENQUIRY && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_ENQUIRY, + userData + ) && + isPeerChat) || + (type !== SESSION_LIST_TYPES.ENQUIRY && + ((isPeerChat && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_PEER_SESSION, + userData + )) || + (!isPeerChat && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_SESSION, + userData + ))))) + ); + }, [activeSession, type, userData]); + + return ( + <> + + + + {tenant?.settings?.featureToolsEnabled && ( + + + + )} + {isSessionAssignAvailable() && ( + +
+ +
+
+ )} + + ); +}; diff --git a/src/components/askerInfo/AskerInfoData.tsx b/src/components/askerInfo/AskerInfoData.tsx index a43dd474a..8c4934bf2 100644 --- a/src/components/askerInfo/AskerInfoData.tsx +++ b/src/components/askerInfo/AskerInfoData.tsx @@ -16,10 +16,7 @@ export const AskerInfoData = () => { const consultingType = useConsultingType(activeSession.item.consultingType); - const userSessionData = getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).sessionData; + const userSessionData = getContact(activeSession).sessionData; const preparedUserSessionData = convertUserDataObjectToArray(userSessionData); @@ -35,6 +32,7 @@ export const AskerInfoData = () => { ? translate( [ `consultingType.${consultingType.id}.titles.default`, + `consultingType.fallback.titles.default`, consultingType.titles.default ], { ns: 'consultingTypes' } diff --git a/src/components/askerInfo/AskerInfoTools.tsx b/src/components/askerInfo/AskerInfoTools.tsx index e40228db4..1d58e57ba 100644 --- a/src/components/askerInfo/AskerInfoTools.tsx +++ b/src/components/askerInfo/AskerInfoTools.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; export const AskerInfoTools = () => { const { t: translate } = useTranslation(); const { activeSession } = useContext(ActiveSessionContext); - const [askerId, setAskerId] = useState(); + const [askerId, setAskerId] = useState(); const openToolsLink = () => { refreshKeycloakAccessToken().then((resp) => { diff --git a/src/components/askerInfo/AskerInfoToolsOptions.tsx b/src/components/askerInfo/AskerInfoToolsOptions.tsx index 8631c18fa..2ad01f6d7 100644 --- a/src/components/askerInfo/AskerInfoToolsOptions.tsx +++ b/src/components/askerInfo/AskerInfoToolsOptions.tsx @@ -246,7 +246,6 @@ export const AskerInfoToolsOptions = ( return (
- { - return ( -
-
{children}
-
- ); -}; +export const Box = ({ children, title, type }: BoxProps) => ( +
+ {title &&
{title}
} +
{children}
+
+); diff --git a/src/components/box/box.module.scss b/src/components/box/box.module.scss new file mode 100644 index 000000000..74a16d486 --- /dev/null +++ b/src/components/box/box.module.scss @@ -0,0 +1,36 @@ +.box { + background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(255, 255, 255, 0.7); + padding: $grid-base-two; + margin-bottom: $grid-base; + border-radius: $box-border-radius; + + @include breakpoint($fromMedium) { + padding: $grid-base-three; + margin-bottom: $grid-base-two; + } + + &--info { + border-color: $form-medium; + color: $form-medium; + } + + &--error { + background-color: $form-error; + color: $form-error; + } + + &--success { + background-color: $form-success; + color: $form-success; + } + + &__title { + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.87); + margin-bottom: 16px; + } +} diff --git a/src/components/box/box.styles.scss b/src/components/box/box.styles.scss deleted file mode 100644 index 93c480e51..000000000 --- a/src/components/box/box.styles.scss +++ /dev/null @@ -1,11 +0,0 @@ -.box { - background: rgba(255, 255, 255, 0.7); - padding: $grid-base-two; - margin-bottom: $grid-base; - border-radius: $box-border-radius; - - @include breakpoint($fromMedium) { - padding: $grid-base-three; - margin-bottom: $grid-base-two; - } -} diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx index 2f6491b88..9258c3dc5 100644 --- a/src/components/checkbox/Checkbox.tsx +++ b/src/components/checkbox/Checkbox.tsx @@ -8,6 +8,7 @@ export interface CheckboxItem { labelId: string; labelClass?: string; label: string; + value?: string; description?: string; checked: boolean; } @@ -24,6 +25,7 @@ export const Checkbox = (props) => { className="checkbox__input" type="checkbox" name={checkboxItem.name} + value={checkboxItem.value} defaultChecked={checkboxItem.checked} /> {checkboxItem.checked && ( diff --git a/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx b/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx index dfb35f333..38c6f6626 100644 --- a/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx +++ b/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx @@ -56,6 +56,7 @@ export const ConsultingTypeAgencySelection = ({ label: translate( [ `consultingType.${consultingType.id}.titles.long`, + `consultingType.fallback.titles.long`, consultingType.titles.long ], { ns: 'consultingTypes' } diff --git a/src/components/downloadICSFile/downloadICSFile.styles.scss b/src/components/downloadICSFile/downloadICSFile.styles.scss index 58b4f66de..f708aaabf 100644 --- a/src/components/downloadICSFile/downloadICSFile.styles.scss +++ b/src/components/downloadICSFile/downloadICSFile.styles.scss @@ -1,6 +1,10 @@ .downloadICSFile { &--flex { display: flex; + + svg { + min-width: 20px; + } } &--primary { diff --git a/src/components/formAccordion/FormAccordion.tsx b/src/components/formAccordion/FormAccordion.tsx index 150379ffd..4a7a141e3 100644 --- a/src/components/formAccordion/FormAccordion.tsx +++ b/src/components/formAccordion/FormAccordion.tsx @@ -266,6 +266,7 @@ export const FormAccordion = ({ label: translate( [ `consultingType.${consultingType.id}.requiredComponents.age.${option.value}`, + `consultingType.fallback.requiredComponents.age.${option.value}`, option.label ], { ns: 'consultingTypes' } diff --git a/src/components/groupChat/GroupChatInfo.tsx b/src/components/groupChat/GroupChatInfo.tsx index dbc085a54..7d04a12fc 100644 --- a/src/components/groupChat/GroupChatInfo.tsx +++ b/src/components/groupChat/GroupChatInfo.tsx @@ -13,7 +13,6 @@ import { Button, ButtonItem, BUTTON_TYPES } from '../button/Button'; import { OVERLAY_FUNCTIONS, Overlay, OverlayItem } from '../overlay/Overlay'; import { apiGetGroupChatInfo, - apiGetGroupMembers, apiPutGroupChat, GROUP_CHAT_API } from '../../api'; @@ -47,6 +46,11 @@ import { GroupChatCopyLinks } from './GroupChatCopyLinks'; import { useAppConfig } from '../../hooks/useAppConfig'; import { useTranslation } from 'react-i18next'; import { getPrettyDateFromMessageDate } from '../../utils/dateHelpers'; +import { ActiveSessionContext } from '../../globalState/provider/ActiveSessionProvider'; +import { + RocketChatUsersOfRoomContext, + RocketChatUsersOfRoomProvider +} from '../../globalState/provider/RocketChatUsersOfRoomProvider'; export const GroupChatInfo = () => { const settings = useAppConfig(); @@ -66,14 +70,10 @@ export const GroupChatInfo = () => { const { userData } = useContext(UserDataContext); const { path: listPath } = useContext(SessionTypeContext); - const [subscriberList, setSubscriberList] = useState(null); const [overlayItem, setOverlayItem] = useState(null); const [overlayActive, setOverlayActive] = useState(false); const [redirectToSessionsList, setRedirectToSessionsList] = useState(false); - const [isUserBanOverlayOpen, setIsUserBanOverlayOpen] = - useState(false); const [isRequestInProgress, setIsRequestInProgress] = useState(false); - const [bannedUsers, setBannedUsers] = useState([]); const [isV2GroupChat, setIsV2GroupChat] = useState(false); const { session: activeSession, ready } = useSession(groupIdFromParam); @@ -106,32 +106,6 @@ export const GroupChatInfo = () => { return; } - if (activeSession.item.active) { - apiGetGroupMembers(activeSession.item.id) - .then((response) => { - const subscribers = response.members.map((member) => ({ - isModerator: isUserModerator({ - chatItem: activeSession.item, - rcUserId: member._id - }), - ...member - })); - setSubscriberList(subscribers); - }) - .catch((error) => { - console.log('error', error); - }); - apiGetGroupChatInfo(activeSession.item.id).then((response) => { - if (response.bannedUsers) { - const decryptedBannedUsers = - response.bannedUsers.map(decodeUsername); - setBannedUsers(decryptedBannedUsers); - } else { - setBannedUsers([]); - } - }); - } - if (activeSession.isGroup && !activeSession.item.consultingType) { setIsV2GroupChat(true); } @@ -266,246 +240,267 @@ export const GroupChatInfo = () => { } return ( -
-
-
- - - -

- {translate('groupChat.info.headline')} -

-
-
-

- {activeSession.item.topic} -

-
-
-
-
-
- - {activeSession.item.active ? ( - - ) : null} -
-

{activeSession.item.topic}

-
- {activeSession.item.active && activeSession.item.subscribed ? ( -
-
- )} - {subscriberList ? ( - subscriberList.map((subscriber, index) => ( -
-
- {subscriber.displayName - ? decodeUsername( - subscriber.displayName - ) - : decodeUsername( - subscriber.username - )} - {isCurrentUserModerator && - !subscriber.isModerator && ( - <> - - { - setBannedUsers([ - ...bannedUsers, - username - ]); - setIsUserBanOverlayOpen( - true - ); - }} - /> - {' '} - { - setIsUserBanOverlayOpen( - false - ); - }} - > - - )} - {isCurrentUserModerator && - bannedUsers.includes( - subscriber.username - ) && ( - - )} -
-
- )) - ) : ( -
-

- {translate( - 'groupChat.info.subscribers.empty' + ) : null} +

+
+ + type="divider" + /> + + {featureGroupChatV2Enabled && isV2GroupChat && ( +
+ +
+ )} +
- )} -
-
- +
+ - {(showCreator || showCreateDate) && ( -
- {showCreator && ( -
-

- {translate( - 'groupChat.info.settings.creator' - )} -

-

- { - activeSession.consultant - .displayName - } -

+ {(showCreator || showCreateDate) && ( +
+ {showCreator && ( +
+

+ {translate( + 'groupChat.info.settings.creator' + )} +

+

+ { + activeSession.consultant + .displayName + } +

+
+ )} + {showCreateDate && ( +
+

+ {translate( + 'groupChat.info.settings.createDate' + )} +

+

+ {getCreationDate( + new Date( + activeSession.item.createdAt + ) + )} +

+
+ )}
)} - {showCreateDate && ( -
+ {preparedSettings.map((item, index) => ( +

- {translate( - 'groupChat.info.settings.createDate' - )} + {item.label}

- {getCreationDate( - new Date( - activeSession.item.createdAt - ) - )} + {item.value}

- )} -
- )} - {preparedSettings.map((item, index) => ( -
-

- {item.label} -

-

- {item.value} -

+ ))} + {isGroupChatOwner(activeSession, userData) && + !activeSession.item.active ? ( + +
- ))} - {isGroupChatOwner(activeSession, userData) && - !activeSession.item.active ? ( - -
+
+ {overlayActive ? ( + + ) : null} +
+ + + ); +}; + +const SubscriberList = ({ + isCurrentUserModerator +}: { + isCurrentUserModerator: boolean; +}) => { + const { t: translate } = useTranslation(); + + const { activeSession } = useContext(ActiveSessionContext); + const { users, moderators } = useContext(RocketChatUsersOfRoomContext); + + const [isUserBanOverlayOpen, setIsUserBanOverlayOpen] = + useState(false); + const [bannedUsers, setBannedUsers] = useState([]); + + useEffect(() => { + if (activeSession.item.active) { + apiGetGroupChatInfo(activeSession.item.id).then((response) => { + if (response.bannedUsers) { + const decryptedBannedUsers = + response.bannedUsers.map(decodeUsername); + setBannedUsers(decryptedBannedUsers); + } else { + setBannedUsers([]); + } + }); + } + }, [activeSession.item.active, activeSession.item.id]); + + return ( + <> + {users ? ( + users.map((subscriber, index) => ( +
+
+ {subscriber.displayName + ? decodeUsername(subscriber.displayName) + : decodeUsername(subscriber.username)} + {isCurrentUserModerator && + !moderators.includes(subscriber._id) && ( + <> + + { + setBannedUsers([ + ...bannedUsers, + username + ]); + setIsUserBanOverlayOpen( + true + ); + }} + /> + {' '} + { + setIsUserBanOverlayOpen(false); + }} + > + + )} + {isCurrentUserModerator && + bannedUsers.includes(subscriber.username) && ( + + )} +
+ )) + ) : ( +
+

+ {translate('groupChat.info.subscribers.empty')} +

-
- {overlayActive ? ( - - ) : null} -
+ )} + ); }; diff --git a/src/components/groupChat/JoinGroupChatView.tsx b/src/components/groupChat/JoinGroupChatView.tsx index f827a66a8..0d0317df4 100644 --- a/src/components/groupChat/JoinGroupChatView.tsx +++ b/src/components/groupChat/JoinGroupChatView.tsx @@ -319,7 +319,13 @@ export const JoinGroupChatView = ({ consultingType?.id ?? 'noConsultingType' }.groupChatRules`; const translatedRules: { [key: string]: string } = - i18n.getResource(i18n.language, 'consultingTypes', transKey) || {}; + i18n.getResource(i18n.language, 'consultingTypes', transKey) || + i18n.getResource( + i18n.language, + 'consultingTypes', + `consultingType.fallback.groupChatRules` + ) || + {}; if (Object.keys(translatedRules).length > 0) { groupChatRules = Object.values(translatedRules); } diff --git a/src/components/messageSubmitInterface/messageSubmitInterfaceComponent.tsx b/src/components/messageSubmitInterface/messageSubmitInterfaceComponent.tsx index e5ef2d00e..f59a552d5 100644 --- a/src/components/messageSubmitInterface/messageSubmitInterfaceComponent.tsx +++ b/src/components/messageSubmitInterface/messageSubmitInterfaceComponent.tsx @@ -12,11 +12,7 @@ import { useHistory } from 'react-router-dom'; import { SendMessageButton } from './SendMessageButton'; import { SESSION_LIST_TYPES } from '../session/sessionHelpers'; import { Checkbox, CheckboxItem } from '../checkbox/Checkbox'; -import { - AUTHORITIES, - getContact, - hasUserAuthority -} from '../../globalState/helpers/stateHelpers'; +import { AUTHORITIES, getContact, hasUserAuthority } from '../../globalState'; import { AnonymousConversationFinishedContext, E2EEContext, @@ -856,17 +852,13 @@ export const MessageSubmitInterfaceComponent = ({ const getMessageSubmitInfo = useCallback((): MessageSubmitInfoInterface => { let infoData; if (activeInfo === INFO_TYPES.ABSENT) { + const contact = getContact(activeSession); infoData = { isInfo: true, infoHeadline: `${ - getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).displayName || - getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).username + contact?.displayName || + contact?.username || + translate('sessionList.user.consultantUnknown') } ${translate('consultant.absent.message')} `, infoMessage: activeSession.consultant.absenceMessage }; diff --git a/src/components/profile/AskerConsultingTypeData.tsx b/src/components/profile/AskerConsultingTypeData.tsx index 20f5087ab..7f353bcbc 100644 --- a/src/components/profile/AskerConsultingTypeData.tsx +++ b/src/components/profile/AskerConsultingTypeData.tsx @@ -33,6 +33,7 @@ export const AskerConsultingTypeData = () => { text={translate( [ `consultingType.${resort.agency.consultingType}.titles.default`, + `consultingType.fallback.titles.default`, consultingTypes.find( (cur) => cur.id === diff --git a/src/components/profile/AskerRegistration.tsx b/src/components/profile/AskerRegistration.tsx index cbc06bf2b..5bf35e942 100644 --- a/src/components/profile/AskerRegistration.tsx +++ b/src/components/profile/AskerRegistration.tsx @@ -114,6 +114,7 @@ export const AskerRegistration: React.FC = () => { label: translate( [ `consultingType.${option.id}.titles.registrationDropdown`, + `consultingType.fallback.titles.registrationDropdown`, option.label ], { ns: 'consultingTypes' } diff --git a/src/components/profile/AskerRegistrationExternalAgencyOverlay.tsx b/src/components/profile/AskerRegistrationExternalAgencyOverlay.tsx index 52a2e862b..084c47e13 100644 --- a/src/components/profile/AskerRegistrationExternalAgencyOverlay.tsx +++ b/src/components/profile/AskerRegistrationExternalAgencyOverlay.tsx @@ -35,6 +35,7 @@ export const AskerRegistrationExternalAgencyOverlay = ({ translate( [ `consultingType.${consultingType.id}.titles.default`, + `consultingType.fallback.titles.default`, consultingType.titles.default ], { ns: 'consultingTypes' } diff --git a/src/components/registration/Registration.tsx b/src/components/registration/Registration.tsx index 35bf44644..68bd3da5a 100644 --- a/src/components/registration/Registration.tsx +++ b/src/components/registration/Registration.tsx @@ -96,6 +96,7 @@ export const Registration = ({ )} ${translate( [ `consultingType.${consultant.agencies[0].consultingTypeRel.id}.titles.long`, + `consultingType.fallback.titles.long`, consultant.agencies[0].consultingTypeRel.titles.long ], { ns: 'consultingTypes' } @@ -131,6 +132,7 @@ export const Registration = ({ )} ${translate( [ `consultingType.${consultingType.id}.titles.long`, + `consultingType.fallback.titles.long`, consultingType.titles.long ], { ns: 'consultingTypes' } @@ -171,6 +173,7 @@ export const Registration = ({ ? translate( [ `consultingType.${consultingType?.id}.titles.welcome`, + `consultingType.fallback.titles.welcome`, consultingType?.titles.welcome ], { ns: 'consultingTypes' } @@ -188,6 +191,7 @@ export const Registration = ({ ? translate( [ `consultingType.${consultingType?.id}.titles.long`, + `consultingType.fallback.titles.long`, consultingType?.titles.long ], { ns: 'consultingTypes' } diff --git a/src/components/registration/RegistrationForm.tsx b/src/components/registration/RegistrationForm.tsx index 7c5afa28b..9ed81fa79 100644 --- a/src/components/registration/RegistrationForm.tsx +++ b/src/components/registration/RegistrationForm.tsx @@ -228,6 +228,7 @@ export const RegistrationForm = () => { ? translate( [ `consultingType.${consultingType.id}.titles.long`, + `consultingType.fallback.titles.long`, consultingType.titles.long ], { ns: 'consultingTypes' } diff --git a/src/components/serviceExplanation/ServiceExplanation.tsx b/src/components/serviceExplanation/ServiceExplanation.tsx index b968bea91..35c3efed9 100644 --- a/src/components/serviceExplanation/ServiceExplanation.tsx +++ b/src/components/serviceExplanation/ServiceExplanation.tsx @@ -44,6 +44,7 @@ export const ServiceExplanation = ({ title: translate( [ `consultingType.${consultingTypeId}.welcomeScreen.anonymous.title`, + `consultingType.fallback.welcomeScreen.anonymous.title`, welcomeScreenConfig?.anonymous.title ?? 'registration.welcomeScreen.info4.title' ], @@ -52,6 +53,7 @@ export const ServiceExplanation = ({ text: translate( [ `consultingType.${consultingTypeId}.welcomeScreen.anonymous.text`, + `consultingType.fallback.welcomeScreen.anonymous.text`, welcomeScreenConfig?.anonymous.text ?? 'registration.welcomeScreen.info4.text' ], diff --git a/src/components/session/AcceptLiveChatView.tsx b/src/components/session/AcceptLiveChatView.tsx index 978c77915..6de33585d 100644 --- a/src/components/session/AcceptLiveChatView.tsx +++ b/src/components/session/AcceptLiveChatView.tsx @@ -78,10 +78,8 @@ export const AcceptLiveChatView = ({ text={`${translate( 'enquiry.anonymous.infoLabel.start' )}${ - getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).username + getContact(activeSession)?.username || + translate('sessionList.user.consultantUnknown') }${translate('enquiry.anonymous.infoLabel.end')}`} />
diff --git a/src/components/session/SessionItemComponent.tsx b/src/components/session/SessionItemComponent.tsx index a4ae8cb8f..36ed77d5d 100644 --- a/src/components/session/SessionItemComponent.tsx +++ b/src/components/session/SessionItemComponent.tsx @@ -394,12 +394,10 @@ export const SessionItemComponent = (props: SessionItemProps) => { { - + { - + @@ -188,7 +188,7 @@ export const SessionView = () => { - + { return (
; @@ -50,12 +50,12 @@ export const GroupChatHeader = ({ bannedUsers }: GroupChatHeaderProps) => { const { releaseToggles } = useAppConfig(); - const [subscriberList, setSubscriberList] = useState([]); const [isUserBanOverlayOpen, setIsUserBanOverlayOpen] = useState(false); const { t } = useTranslation(['common', 'consultingTypes', 'agencies']); const { activeSession } = useContext(ActiveSessionContext); + const { users, moderators } = useContext(RocketChatUsersOfRoomContext); const { userData } = useContext(UserDataContext); const { type, path: listPath } = useContext(SessionTypeContext); const sessionListTab = useSearchParam('sessionListTab'); @@ -77,10 +77,7 @@ export const GroupChatHeader = ({ rcUserId: getValueFromCookie('rc_uid') }); - const userSessionData = getContact( - activeSession, - t('sessionList.user.consultantUnknown') - ).sessionData; + const userSessionData = getContact(activeSession)?.sessionData || {}; const isAskerInfoAvailable = () => !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && consultingType?.showAskerProfile && @@ -94,21 +91,7 @@ export const GroupChatHeader = ({ const handleFlyout = (e) => { if (!isSubscriberFlyoutOpen) { - apiGetGroupMembers(activeSession.item.id) - .then((response) => { - const subscribers = response.members.map((member) => ({ - isModerator: isUserModerator({ - chatItem: activeSession.item, - rcUserId: member._id - }), - ...member - })); - setSubscriberList(subscribers); - setIsSubscriberFlyoutOpen(true); - }) - .catch((error) => { - console.error(error); - }); + setIsSubscriberFlyoutOpen(true); } else if (e.target.id === 'subscriberButton') { setIsSubscriberFlyoutOpen(false); } @@ -221,109 +204,111 @@ export const GroupChatHeader = ({ {isSubscriberFlyoutOpen && (
    - {subscriberList.map( - (subscriber, index) => ( -
  • ( +
  • { + if ( !bannedUsers.includes( subscriber.username - ) && - !subscriber.isModerator - ? 'has-flyout' - : '' + ) + ) { + setFlyoutOpenId( + subscriber._id + ); } - key={index} - onClick={() => { - if ( - !bannedUsers.includes( - subscriber.username - ) - ) { - setFlyoutOpenId( - subscriber._id - ); - } - }} - > - - {decodeUsername( - subscriber.displayName || - subscriber.username - )} - - {isCurrentUserModerator && - !subscriber.isModerator && ( - <> - + + {decodeUsername( + subscriber.displayName || + subscriber.username + )} + + {isCurrentUserModerator && + !moderators.includes( + subscriber._id + ) && ( + <> + + setFlyoutOpenId( + null + ) + } + > + - setFlyoutOpenId( - null - ) - } - > - { - setIsUserBanOverlayOpen( - true - ); - }} - /> - {' '} - { + handleUserBan={() => { setIsUserBanOverlayOpen( - false + true ); }} - > - - )} - {isCurrentUserModerator && - bannedUsers.includes( - subscriber.username - ) && ( - + {' '} + - )} -
  • - ) - )} + handleOverlay={() => { + setIsUserBanOverlayOpen( + false + ); + }} + > + + )} + {isCurrentUserModerator && + bannedUsers.includes( + subscriber.username + ) && ( + + )} + + ))}
)} diff --git a/src/components/sessionHeader/SessionHeaderComponent.tsx b/src/components/sessionHeader/SessionHeaderComponent.tsx index 9f6dd3239..4308ef466 100644 --- a/src/components/sessionHeader/SessionHeaderComponent.tsx +++ b/src/components/sessionHeader/SessionHeaderComponent.tsx @@ -19,7 +19,10 @@ import { SESSION_LIST_TYPES } from '../session/sessionHelpers'; import { SessionMenu } from '../sessionMenu/SessionMenu'; -import { convertUserDataObjectToArray } from '../profile/profileHelpers'; +import { + convertUserDataObjectToArray, + getUserDataTranslateBase +} from '../profile/profileHelpers'; import { ReactComponent as BackIcon } from '../../resources/img/icons/arrow-left.svg'; import { ActiveSessionContext } from '../../globalState/provider/ActiveSessionProvider'; import './sessionHeader.styles'; @@ -27,6 +30,7 @@ import './sessionHeader.yellowTheme.styles'; import { useSearchParam } from '../../hooks/useSearchParams'; import { useTranslation } from 'react-i18next'; import { GroupChatHeader } from './GroupChatHeader'; +import { useAppConfig } from '../../hooks/useAppConfig'; export interface SessionHeaderProps { consultantAbsent?: SessionConsultantInterface; @@ -44,29 +48,20 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { const { activeSession } = useContext(ActiveSessionContext); const { userData } = useContext(UserDataContext); const consultingType = useConsultingType(activeSession.item.consultingType); + const settings = useAppConfig(); + + const contact = getContact(activeSession); + const userSessionData = contact?.sessionData; - const username = getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).username; - const displayName = getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).displayName; - const userSessionData = getContact( - activeSession, - translate('sessionList.user.consultantUnknown') - ).sessionData; const preparedUserSessionData = hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData) && userSessionData && !activeSession.isLive ? convertUserDataObjectToArray(userSessionData) : null; - const translateBase = - activeSession.item.consultingType === 0 - ? 'user.userAddiction' - : 'user.userU25'; + const translateBase = getUserDataTranslateBase( + activeSession.item.consultingType + ); const [isSubscriberFlyoutOpen, setIsSubscriberFlyoutOpen] = useState(false); const sessionListTab = useSearchParam('sessionListTab'); @@ -104,13 +99,17 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { } }; + const enquiryUserProfileCondition = + typeof settings?.user?.profile?.visibleOnEnquiry === 'function' + ? settings.user.profile.visibleOnEnquiry(userSessionData) + : settings?.user?.profile?.visibleOnEnquiry; + const isAskerInfoAvailable = () => !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && consultingType?.showAskerProfile && activeSession.isSession && !activeSession.isLive && - ((type === SESSION_LIST_TYPES.ENQUIRY && - Object.entries(userSessionData).length !== 0) || + ((type === SESSION_LIST_TYPES.ENQUIRY && enquiryUserProfileCondition) || SESSION_LIST_TYPES.ENQUIRY !== type); if (activeSession.isGroup) { @@ -170,8 +169,16 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { !isAskerInfoAvailable() })} > - {hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && ( -

{displayName || username}

+ {(hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) || + hasUserAuthority( + AUTHORITIES.ANONYMOUS_DEFAULT, + userData + )) && ( +

+ {contact?.displayName || + contact?.username || + translate('sessionList.user.consultantUnknown')} +

)} {hasUserAuthority( AUTHORITIES.CONSULTANT_DEFAULT, @@ -179,16 +186,22 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { ) ? ( isAskerInfoAvailable() ? ( -

{username}

+

+ {contact?.username || + translate( + 'sessionList.user.consultantUnknown' + )} +

) : ( -

{username}

+

+ {contact?.username || + translate( + 'sessionList.user.consultantUnknown' + )} +

) ) : null} - {hasUserAuthority( - AUTHORITIES.ANONYMOUS_DEFAULT, - userData - ) &&

{displayName || username}

}
{ ? translate( [ `consultingType.${consultingType.id}.titles.short`, + `consultingType.fallback.titles.short`, consultingType.titles.short ], { ns: 'consultingTypes' } diff --git a/src/components/sessionMenu/SessionMenu.tsx b/src/components/sessionMenu/SessionMenu.tsx index 2e57e131c..13061348a 100644 --- a/src/components/sessionMenu/SessionMenu.tsx +++ b/src/components/sessionMenu/SessionMenu.tsx @@ -10,7 +10,6 @@ import { generatePath, Link, Redirect, useHistory } from 'react-router-dom'; import { AnonymousConversationFinishedContext, AUTHORITIES, - ExtendedSessionInterface, hasUserAuthority, SessionItemInterface, SessionTypeContext, @@ -66,6 +65,7 @@ import { useSearchParam } from '../../hooks/useSearchParams'; import { useAppConfig } from '../../hooks/useAppConfig'; import { useTranslation } from 'react-i18next'; import { LegalLinksContext } from '../../globalState/provider/LegalLinksProvider'; +import { RocketChatUsersOfRoomContext } from '../../globalState/provider/RocketChatUsersOfRoomProvider'; type TReducedSessionItemInterface = Omit< SessionItemInterface, @@ -77,6 +77,7 @@ export interface SessionMenuProps { isAskerInfoAvailable: boolean; isJoinGroupChatView?: boolean; bannedUsers?: string[]; + subscribers?: any[]; } export const SessionMenu = (props: SessionMenuProps) => { @@ -584,7 +585,6 @@ export const SessionMenu = (props: SessionMenuProps) => { {activeSession.isGroup && ( { }; const SessionMenuFlyoutGroup = ({ - activeSession, groupChatInfoLink, editGroupChatSettingsLink, handleLeaveGroupChat, handleStopGroupChat, bannedUsers }: { - activeSession: ExtendedSessionInterface; groupChatInfoLink: string; editGroupChatSettingsLink: string; handleStopGroupChat: MouseEventHandler; @@ -640,11 +638,14 @@ const SessionMenuFlyoutGroup = ({ }) => { const { t: translate } = useTranslation(); const { userData } = useContext(UserDataContext); + const { activeSession } = useContext(ActiveSessionContext); + const { moderators } = useContext(RocketChatUsersOfRoomContext); return ( <> {activeSession.item.subscribed && - !bannedUsers?.includes(userData.userName) && ( + !bannedUsers?.includes(userData.userName) && + moderators.length > 1 && (
{ return labelContent; }; + // Do not render text component if content is empty + if (!props.title && !props.text) return null; + return (

{ useEffect(() => { apiGetAskerSessionList().then(({ sessions }) => { - const session = sessions.find((s) => !!s.consultant); + const session = sessions.find((s) => !!s.agency); setSession(session); const consultant = session?.consultant; const agencyId = session?.agency?.id; diff --git a/src/containers/bookings/components/Calcom/cal.styles.scss b/src/containers/bookings/components/Calcom/cal.styles.scss index 3e4b25d29..e89900607 100644 --- a/src/containers/bookings/components/Calcom/cal.styles.scss +++ b/src/containers/bookings/components/Calcom/cal.styles.scss @@ -1,10 +1,7 @@ .contentWrapper__booking { > div { - height: calc(100% - 75px); - - @include breakpoint($fromLarge) { - height: 100%; - } + overflow: auto; + height: 100%; } cal-inline { @@ -13,7 +10,6 @@ @include breakpoint($fromLarge) { align-items: center; - padding-bottom: 100px; } iframe { diff --git a/src/containers/bookings/components/NoBookings/noBookingsBooked.tsx b/src/containers/bookings/components/NoBookings/noBookingsBooked.tsx index 1af5d238e..1ae636e0d 100644 --- a/src/containers/bookings/components/NoBookings/noBookingsBooked.tsx +++ b/src/containers/bookings/components/NoBookings/noBookingsBooked.tsx @@ -60,8 +60,9 @@ export const NoBookingsBooked: React.FC = ({ sessions }) => { ${sessions ?.filter((session) => session.agency !== null) ?.map( - (consultant) => - consultant.consultant.username + ({ consultant }) => + consultant.displayName || + consultant.username )}:`} type="standard" /> diff --git a/src/containers/bookings/components/booking.styles.scss b/src/containers/bookings/components/booking.styles.scss index 29bcc58b7..295a491bc 100644 --- a/src/containers/bookings/components/booking.styles.scss +++ b/src/containers/bookings/components/booking.styles.scss @@ -25,6 +25,7 @@ $headerHeight: 80px; &__header { display: flex; + flex: 0; align-items: center; justify-content: space-between; padding: $grid-base-three; @@ -116,12 +117,17 @@ $headerHeight: 80px; } } + &__wrapper { + display: flex; + flex-direction: column; + } + &__innerWrapper { display: flex; + flex: 1; flex-direction: column; padding: 0 $grid-base-three; overflow-x: hidden; - height: 100%; &-event { display: grid; @@ -217,6 +223,7 @@ $headerHeight: 80px; } p { + word-break: normal; margin-left: 0.5rem; } } @@ -276,6 +283,7 @@ $headerHeight: 80px; margin-bottom: 0.5rem; p { + word-break: normal; margin-left: 0.5rem; } } @@ -372,10 +380,34 @@ $headerHeight: 80px; &__video-link-grid { display: none; - grid-template-columns: 0.465fr 1fr 0.42fr; + grid-template-columns: 0.29fr 1fr 0.42fr; min-height: 32px; align-items: flex-end; + @media only screen and (min-width: 950px) { + grid-template-columns: 0.33fr 1fr 0.42fr; + } + + @media only screen and (min-width: 1010px) { + grid-template-columns: 0.35fr 1fr 0.42fr; + } + + @media only screen and (min-width: 1050px) { + grid-template-columns: 0.37fr 1fr 0.42fr; + } + + @media only screen and (min-width: 1075px) { + grid-template-columns: 0.43fr 1fr 0.42fr; + } + + @media only screen and (min-width: 1095px) { + grid-template-columns: 0.44fr 1fr 0.42fr; + } + + @media only screen and (min-width: 1200px) { + grid-template-columns: 0.465fr 1fr 0.42fr; + } + @include breakpoint($fromXXLarge) { grid-template-columns: 0.48fr 1fr 0.405fr; } @@ -393,15 +425,28 @@ $headerHeight: 80px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; + position: relative; + left: 20px; + + @media only screen and (min-width: 1077px) { + position: initial; + left: 0; + } } @include breakpoint($fromLarge) { - word-break: keep-all; display: flex; align-items: center; svg { - margin-left: 0.75rem; + position: relative; + right: -50px; + + @media only screen and (min-width: 1077px) { + position: initial; + right: 0; + margin-left: 0.75rem; + } } } } diff --git a/src/containers/registration/components/ConsultingTypeSelection/index.tsx b/src/containers/registration/components/ConsultingTypeSelection/index.tsx index f7b39fca6..c5b6a503f 100644 --- a/src/containers/registration/components/ConsultingTypeSelection/index.tsx +++ b/src/containers/registration/components/ConsultingTypeSelection/index.tsx @@ -29,6 +29,7 @@ export const ConsultingTypeSelection = ({ label: t( [ `consultingType.${consultingType.id}.titles.long`, + `consultingType.fallback.titles.long`, consultingType.titles.long ], { ns: 'consultingTypes' } diff --git a/src/extensions/components/askerInfo/AskerInfoContent.tsx b/src/extensions/components/askerInfo/AskerInfoContent.tsx new file mode 100644 index 000000000..809259fd8 --- /dev/null +++ b/src/extensions/components/askerInfo/AskerInfoContent.tsx @@ -0,0 +1,146 @@ +import * as React from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { SESSION_LIST_TYPES } from '../../../components/session/sessionHelpers'; +import { + AUTHORITIES, + ConsultingSessionDataInterface, + hasUserAuthority, + SessionTypeContext, + TenantContext, + TopicSessionInterface, + UserDataContext +} from '../../../globalState'; +import { AskerInfoAssign } from '../../../components/askerInfo/AskerInfoAssign'; +import '../../../components/askerInfo/askerInfo.styles'; +import { ActiveSessionContext } from '../../../globalState/provider/ActiveSessionProvider'; +import { AskerInfoTools } from '../../../components/askerInfo/AskerInfoTools'; +import { ProfileBox } from './ProfileBox'; +import { ProfileDataItem } from './ProfileDataItem'; +import { AskerInfoDocumentation } from './AskerInfoDocumentation'; +import { apiGetUserDataBySessionId } from '../../../api/apiGetUserDataBySessionId'; +import { useTranslation } from 'react-i18next'; +import { Box, BoxTypes } from '../../../components/box/Box'; + +export const AskerInfoContent = () => { + const { t: translate } = useTranslation(); + const { tenant } = useContext(TenantContext); + const { activeSession } = useContext(ActiveSessionContext); + const { userData } = useContext(UserDataContext); + const [sessionData, setSessionData] = + useState(null); + + const { type } = useContext(SessionTypeContext); + + useEffect(() => { + if (activeSession?.item?.id) { + apiGetUserDataBySessionId(activeSession.item.id) + .then(setSessionData) + .catch(console.log); + } + }, [activeSession?.item?.id]); + + const isSessionAssignAvailable = useCallback(() => { + const isPeerChat = activeSession.item.isPeerChat; + return ( + !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && + !activeSession.isLive && + !activeSession.isGroup && + ((type === SESSION_LIST_TYPES.ENQUIRY && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_ENQUIRY, + userData + ) && + isPeerChat) || + (type !== SESSION_LIST_TYPES.ENQUIRY && + ((isPeerChat && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_PEER_SESSION, + userData + )) || + (!isPeerChat && + hasUserAuthority( + AUTHORITIES.ASSIGN_CONSULTANT_TO_SESSION, + userData + ))))) + ); + }, [activeSession, type, userData]); + + const translateKeys = { + gender: `profile.gender.options.${sessionData?.gender?.toLowerCase()}`, + counselling: `profile.counsellingRelation.${sessionData?.counsellingRelation?.toLowerCase()}` + }; + + return ( + <> + {!sessionData && ( + + {translate('profile.enquiry.notice')} + + )} +

+ {sessionData && ( + + + + + + + )} + + {tenant?.settings?.featureToolsEnabled && sessionData?.id && ( + + + + )} + + + {(sessionData?.mainTopic || activeSession?.item?.topic) && ( + + )} + + {sessionData?.topics?.length > 0 && ( + name) + .join(', ')} + /> + )} + + + {tenant?.settings?.featureToolsEnabled && sessionData?.id && ( + + + + )} + + {isSessionAssignAvailable() && ( + + + + )} +
+ + ); +}; diff --git a/src/extensions/components/askerInfo/AskerInfoDocumentation.tsx b/src/extensions/components/askerInfo/AskerInfoDocumentation.tsx new file mode 100644 index 000000000..cbccf64fb --- /dev/null +++ b/src/extensions/components/askerInfo/AskerInfoDocumentation.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { apiGetUserDataBySessionId } from '../../../api/apiGetUserDataBySessionId'; +import { ActiveSessionContext } from '../../../globalState/provider/ActiveSessionProvider'; +import { ReactComponent as NewWindow } from '../../../resources/img/icons/new-window.svg'; +import { endpoints } from '../../../resources/scripts/endpoints'; +import { refreshKeycloakAccessToken } from '../../../components/sessionCookie/refreshKeycloakAccessToken'; +import { Text } from '../../../components/text/Text'; +import '../../../components/askerInfo/askerInfoTools.styles'; +import { useTranslation } from 'react-i18next'; + +export const AskerInfoDocumentation = () => { + const { t: translate } = useTranslation(); + const { activeSession } = useContext(ActiveSessionContext); + const [askerItemID, setAskerItemId] = useState(); + + const openToolsLink = () => { + refreshKeycloakAccessToken().then((resp) => { + const accessToken = resp.access_token; + window.open( + `${endpoints.budibaseTools( + activeSession.consultant.id + )}/consultantview?userId=${askerItemID}&access_token=${accessToken}`, + '_blank', + 'noopener' + ); + }); + }; + + useEffect(() => { + apiGetUserDataBySessionId(activeSession.item.id).then((resp) => { + setAskerItemId(resp.askerId); + }); + }, [activeSession?.item?.id, askerItemID]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + + + + ); +}; diff --git a/src/extensions/components/askerInfo/ProfileBox/index.tsx b/src/extensions/components/askerInfo/ProfileBox/index.tsx new file mode 100644 index 000000000..ce6ba574e --- /dev/null +++ b/src/extensions/components/askerInfo/ProfileBox/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box } from '../../../../components/box/Box'; +import './styles'; + +interface ProfileBoxProps { + title: string; + children: React.ReactNode; +} + +export const ProfileBox = ({ title, children }: ProfileBoxProps) => { + const { t: translate } = useTranslation(); + return ( +
+
+ {children} +
+
+ ); +}; diff --git a/src/extensions/components/askerInfo/ProfileBox/styles.scss b/src/extensions/components/askerInfo/ProfileBox/styles.scss new file mode 100644 index 000000000..50aae7b26 --- /dev/null +++ b/src/extensions/components/askerInfo/ProfileBox/styles.scss @@ -0,0 +1,19 @@ +.profilebox { + flex-basis: 50%; + height: auto !important; + + &__content { + padding: 12.5px; + + & .button-as-link { + border: none; + background: none; + padding: 0 !important; + text-decoration: $link-text-decoration; + + &:hover { + cursor: pointer; + } + } + } +} diff --git a/src/extensions/components/askerInfo/ProfileDataItem/index.tsx b/src/extensions/components/askerInfo/ProfileDataItem/index.tsx new file mode 100644 index 000000000..8c8c91bcd --- /dev/null +++ b/src/extensions/components/askerInfo/ProfileDataItem/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ProfileDataItemProps { + title: string; + content: string; +} + +export const ProfileDataItem = ({ title, content }: ProfileDataItemProps) => { + const { t: translate } = useTranslation(); + return ( +
+

{translate(title)}

+

{content}

+
+ ); +}; diff --git a/src/extensions/components/legalInformationLinks/Imprint.tsx b/src/extensions/components/legalInformationLinks/Imprint.tsx new file mode 100644 index 000000000..88fac04a1 --- /dev/null +++ b/src/extensions/components/legalInformationLinks/Imprint.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { useTenant } from '../../../../'; +import { LegalPageWrapper } from '../legalPageWrapper/LegalPageWrapper'; +import useDocumentTitle from '../../utils/useDocumentTitle'; +import { useTranslation } from 'react-i18next'; + +export const Imprint = () => { + const [t] = useTranslation(); + const tenant = useTenant(); + useDocumentTitle(t('profile.footer.imprint')); + return ( + + ); +}; diff --git a/src/extensions/components/legalInformationLinks/Privacy.tsx b/src/extensions/components/legalInformationLinks/Privacy.tsx new file mode 100644 index 000000000..9b9ac6e46 --- /dev/null +++ b/src/extensions/components/legalInformationLinks/Privacy.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { useTenant } from '../../../../'; +import { LegalPageWrapper } from '../legalPageWrapper/LegalPageWrapper'; +import useDocumentTitle from '../../utils/useDocumentTitle'; +import { useTranslation } from 'react-i18next'; + +export const Privacy = () => { + const [t] = useTranslation(); + const tenant = useTenant(); + useDocumentTitle(t('profile.footer.dataprotection')); + return ( + + ); +}; diff --git a/src/extensions/components/legalInformationLinks/TermsAndConditions.tsx b/src/extensions/components/legalInformationLinks/TermsAndConditions.tsx new file mode 100644 index 000000000..d4639dbb1 --- /dev/null +++ b/src/extensions/components/legalInformationLinks/TermsAndConditions.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { useTenant } from '../../../../'; +import { LegalPageWrapper } from '../legalPageWrapper/LegalPageWrapper'; +import useDocumentTitle from '../../utils/useDocumentTitle'; +import { useTranslation } from 'react-i18next'; + +export const TermsAndConditions = () => { + const [t] = useTranslation(); + const tenant = useTenant(); + useDocumentTitle(t('legal.termsAndConditions.label')); + return ( + + ); +}; diff --git a/src/extensions/components/legalPageWrapper/LegalPageWrapper.tsx b/src/extensions/components/legalPageWrapper/LegalPageWrapper.tsx new file mode 100644 index 000000000..6488c42f3 --- /dev/null +++ b/src/extensions/components/legalPageWrapper/LegalPageWrapper.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; +import * as React from 'react'; + +import { Stage } from '../stage/stage'; +import htmlParser from '../../resources/scripts/util/htmlParser'; +import './legalPageWrapper.styles.scss'; + +export interface LegalPageWrapperProps { + className?: string; + content: string; +} +export const LegalPageWrapper = ({ + className, + content +}: LegalPageWrapperProps) => { + return ( +
+ +
+
+ {typeof content === 'string' && htmlParser(content)} +
+
+
+ ); +}; diff --git a/src/extensions/components/legalPageWrapper/legalPageWrapper.styles.scss b/src/extensions/components/legalPageWrapper/legalPageWrapper.styles.scss new file mode 100644 index 000000000..dee805ed1 --- /dev/null +++ b/src/extensions/components/legalPageWrapper/legalPageWrapper.styles.scss @@ -0,0 +1,42 @@ +.legalPageWrapper { + @include breakpoint($fromLarge) { + .stage { + display: flex; + } + } + + .template { + h2 + p, + h3 + p, + h4 + p { + margin-top: 0.3rem; + } + ol { + counter-reset: item; + li { + display: block; + } + + li::before { + content: counters(item, '.') '. '; + counter-increment: item; + font-weight: bold; + } + + ol { + counter-reset: item; + } + } + } + + .stageLayout__content { + align-items: flex-start; + justify-content: flex-start; + padding-top: 120px; + + @include breakpoint($fromLarge) { + width: calc(60vw - 160px); + left: calc(40vw + 80px); + } + } +} diff --git a/src/extensions/components/registration/AgencyFields/Agency/index.tsx b/src/extensions/components/registration/AgencyFields/Agency/index.tsx new file mode 100644 index 000000000..1876c57cc --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/Agency/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { AgencyDataInterface } from '../../../../../globalState'; +import { AgencyInfo } from '../../../../../components/agencySelection/AgencyInfo'; +import { AgencyLanguages } from '../../../../../components/agencySelection/AgencyLanguages'; +import { RadioButton } from '../../../../../components/radioButton/RadioButton'; + +interface AgencySelectionFormFieldProps { + onChange: (value: number) => void; + value?: number; + agency: AgencyDataInterface; +} + +export const AgencyRadioButtonForm = ({ + agency, + value, + onChange +}: AgencySelectionFormFieldProps) => ( +
+ onChange(agency?.id)} + type="smaller" + value={agency.id.toString()} + checked={value === agency?.id} + inputId={agency.id.toString()} + label={agency.name} + /> + + +
+); diff --git a/src/extensions/components/registration/AgencyFields/AgencySelection/agencySelection.styles.scss b/src/extensions/components/registration/AgencyFields/AgencySelection/agencySelection.styles.scss new file mode 100644 index 000000000..fdbbe38f3 --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/AgencySelection/agencySelection.styles.scss @@ -0,0 +1,3 @@ +.registrationDigi__noAgencyFound { + margin-top: 24px; +} diff --git a/src/extensions/components/registration/AgencyFields/AgencySelection/index.tsx b/src/extensions/components/registration/AgencyFields/AgencySelection/index.tsx new file mode 100644 index 000000000..e945b90a6 --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/AgencySelection/index.tsx @@ -0,0 +1,266 @@ +import { Field, FieldContext } from 'rc-field-form'; +import { HOOK_MARK } from 'rc-field-form/lib/FieldContext'; +import React, { useEffect, useState } from 'react'; +import { apiAgencySelection, FETCH_ERRORS } from '../../../../../api'; +import { + ConsultingTypeBasicInterface, + AgencyDataInterface +} from '../../../../../globalState'; +import { PinIcon } from '../../../../../resources/img/icons'; +import { VALID_POSTCODE_LENGTH } from '../../../../../components/agencySelection/agencySelectionHelpers'; +import { PreselectedAgency } from '../../../../../containers/registration/components/PreSelectedAgency/PreselectedAgency'; +import { Loading } from '../../../../../components/app/Loading'; +import { InputField } from '../../../../../components/inputField/InputField'; +import { Text } from '../../../../../components/text/Text'; +import { AgencyRadioButtonForm } from '../Agency'; +import { NoAgencyFound } from '../NoAgencyFound'; +import './agencySelection.styles.scss'; +import { useTranslation } from 'react-i18next'; +import { setValueInCookie } from '../../../../../components/sessionCookie/accessSessionCookie'; + +interface AgencySelectionFormFieldProps { + preselectedAgencies?: AgencyDataInterface[]; + consultingType: ConsultingTypeBasicInterface; +} + +const PostCodeInput = ({ + value, + onChange +}: { + value?: string; + onChange?: (value: string) => void; +}) => { + const { t: translate } = useTranslation(); + + return ( + + }} + inputHandle={(e) => onChange(e.target.value)} + /> + ); +}; + +const AgencyRadioInput = ({ + agencies, + value, + onChange +}: { + agencies: AgencyDataInterface[]; + value?: number; + onChange?: (value: number) => void; +}) => { + const field = React.useContext(FieldContext); + return ( + <> + {agencies?.map((agency: AgencyDataInterface) => ( + { + onChange(e); + + setValueInCookie( + 'tenantId', + agency?.tenantId ? `${agency?.tenantId}` : '0' + ); + + field.setFieldValue( + 'consultingTypeId', + agency.consultingType + ); + }} + /> + ))} + + ); +}; + +const REGEX_POSTCODE = /\d{5}/; +export const AgencySelection = ({ + consultingType, + preselectedAgencies +}: AgencySelectionFormFieldProps) => { + const field = React.useContext(FieldContext); + const [isLoading, setIsLoading] = useState(false); + const [agencies, setAgencies] = useState([ + ...(preselectedAgencies || []) + ]); + const { + mainTopicId, + gender, + age, + postCode: postcode, + counsellingRelation + } = field.getFieldsValue(); + const isValidToRequestData = + !preselectedAgencies?.length && + Number(mainTopicId) >= 0 && + age && + gender && + counsellingRelation && + !!postcode?.match(REGEX_POSTCODE); + + const { t: translate } = useTranslation(); + // Only runs when no preselected agencies are provided + useEffect(() => { + if (isValidToRequestData) { + setIsLoading(true); + apiAgencySelection({ + postcode, + consultingType: consultingType?.id, + topicId: mainTopicId, + age, + gender, + counsellingRelation + }) + .then((response) => { + setAgencies(response); + if (response.length === 1) { + const agency = response[0]; + field.setFieldValue( + 'consultingTypeId', + agency.consultingType + ); + field.getInternalHooks(HOOK_MARK).dispatch({ + type: 'updateValue', + namePath: ['agencyId'], + value: agency.id + }); + setValueInCookie( + 'tenantId', + agency?.tenantId ? `${agency?.tenantId}` : '0' + ); + } + }) + .catch((err) => { + if (err.message === FETCH_ERRORS.EMPTY) { + return setAgencies([]); + } + return Promise.reject(err); + }) + .finally(() => setIsLoading(false)); + } else if (!preselectedAgencies?.length) { + field.setFieldValue('agencyId', null); + setAgencies([]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + consultingType?.id, + mainTopicId, + age, + gender, + postcode, + isValidToRequestData + ]); + + const introItemsTranslations = !!preselectedAgencies?.length + ? [ + 'registration.agencyPreselected.intro.point1', + 'registration.agencyPreselected.intro.point2' + ] + : [ + 'registration.agencySelection.intro.point1', + 'registration.agencySelection.intro.point2', + 'registration.agencySelection.intro.point3' + ]; + + return ( +
+
+ +
+ +
    + {introItemsTranslations.map( + (introItemTranslation, i) => ( +
  • + +
  • + ) + )} +
+
+
+ + + + + + {isLoading && } + {!isLoading && isValidToRequestData && agencies.length === 0 && ( + + )} + {!isLoading && + (isValidToRequestData || preselectedAgencies?.length > 1) && + agencies.length > 0 && ( + <> +
+

+ {translate( + 'registration.agencySelection.title.start' + )}{' '} + {postcode} + {translate( + 'registration.agencySelection.title.end' + )} +

+
+ + + + + + )} + + {preselectedAgencies?.length === 1 && ( + <> + + + )} +
+ ); +}; diff --git a/src/extensions/components/registration/AgencyFields/NoAgencyFound/index.tsx b/src/extensions/components/registration/AgencyFields/NoAgencyFound/index.tsx new file mode 100644 index 000000000..64b23c44c --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/NoAgencyFound/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Notice } from '../../../../../components/notice/Notice'; +import { Text } from '../../../../../components/text/Text'; +import { useTranslation } from 'react-i18next'; + +export const NoAgencyFound = ({ className }: { className?: string }) => { + const { t: translate } = useTranslation(); + return ( +
+ + + +
+ ); +}; diff --git a/src/extensions/components/registration/AgencyFields/index.tsx b/src/extensions/components/registration/AgencyFields/index.tsx new file mode 100644 index 000000000..9a09db20d --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/index.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { FieldContext } from 'rc-field-form'; +import { + ConsultingTypeBasicInterface, + AgencyDataInterface +} from '../../../../globalState'; +import { InputFormField } from '../InputFormField'; +import { AgencySelection } from './AgencySelection'; +import { useTranslation } from 'react-i18next'; +import { Text } from '../../../../components/text/Text'; + +interface AgencySelectionFormFieldProps { + preselectedAgencies: AgencyDataInterface[]; + consultingType: ConsultingTypeBasicInterface; + value?: string; +} + +export const AgencySelectionFormField = ({ + consultingType, + preselectedAgencies +}: AgencySelectionFormFieldProps) => { + const field = React.useContext(FieldContext); + const { t: translate } = useTranslation(); + const { mainTopicId, gender, age, counsellingRelation } = + field.getFieldsValue(); + + return ( + <> + {!!( + Number(mainTopicId) >= 0 && + gender && + age && + counsellingRelation + ) || preselectedAgencies.length > 0 ? ( + + ) : ( +
+ +
+ )} + + + + + + ); +}; diff --git a/src/extensions/components/registration/CheckboxFormField/index.tsx b/src/extensions/components/registration/CheckboxFormField/index.tsx new file mode 100644 index 000000000..368b326e4 --- /dev/null +++ b/src/extensions/components/registration/CheckboxFormField/index.tsx @@ -0,0 +1,55 @@ +import { Field } from 'rc-field-form'; +import * as React from 'react'; +import { + Checkbox, + CheckboxItem +} from '../../../../components/checkbox/Checkbox'; + +export interface CheckboxFormFieldProps { + name: string; + labelClass?: string; + label: string; + value?: string; + localValue: string; + onChange?: (v: string) => void; +} + +const CheckBoxLocal = ({ + value, + onChange, + localValue, + ...rest +}: CheckboxFormFieldProps) => { + const onLocalChange = React.useCallback( + (v) => { + onChange(value === localValue ? '' : localValue); + }, + [onChange, value, localValue] + ); + + const id = `checkbox-${rest.label.replace(/\s/g, '-')}`; + const item = { + labelClass: rest.labelClass || '', + inputId: id, + name: '', + labelId: id, + value: localValue, + label: rest.label, + checked: value === localValue + } as unknown as CheckboxItem; + return ( + e.key === 'Space' && onLocalChange(e)} + /> + ); +}; + +export const CheckboxFormField = (props: CheckboxFormFieldProps) => { + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/CheckboxGroupFormField/index.tsx b/src/extensions/components/registration/CheckboxGroupFormField/index.tsx new file mode 100644 index 000000000..568671fb1 --- /dev/null +++ b/src/extensions/components/registration/CheckboxGroupFormField/index.tsx @@ -0,0 +1,56 @@ +import { Field } from 'rc-field-form'; +import * as React from 'react'; +import { + Checkbox, + CheckboxItem +} from '../../../../components/checkbox/Checkbox'; + +export interface CheckboxFormFieldProps { + name: string; + labelClass?: string; + label: string; + value?: number[]; + localValue: number; + onChange?: (v: number[]) => void; +} + +const CheckBoxLocal = ({ + value, + onChange, + localValue, + ...rest +}: CheckboxFormFieldProps) => { + const onLocalChange = React.useCallback(() => { + const alreadyExists = value.indexOf(localValue); + if (alreadyExists === -1) { + onChange([...value, localValue]); + } else { + onChange(value.filter((v) => v !== localValue)); + } + }, [value, localValue, onChange]); + const id = `checkbox-${rest.label.replace(/\s/g, '-')}`; + const item = { + labelClass: rest.labelClass || '', + inputId: id, + name: '', + labelId: id, + value: localValue, + label: rest.label, + checked: value.indexOf(localValue) !== -1 + } as unknown as CheckboxItem; + return ( + e.key === 'Space' && onLocalChange()} + /> + ); +}; + +export const CheckboxGroupFormField = (props: CheckboxFormFieldProps) => { + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/FormAccordion/formAccordion.styles.scss b/src/extensions/components/registration/FormAccordion/formAccordion.styles.scss new file mode 100644 index 000000000..65327e66f --- /dev/null +++ b/src/extensions/components/registration/FormAccordion/formAccordion.styles.scss @@ -0,0 +1,109 @@ +.formAccordionDigi { + &Title { + flex-grow: 1; + text-align: left; + padding-left: 32px; + } + + &__StepNumber + &Title { + padding-left: 0; + } + + &__Panel:first-child > &__PanelHeader { + border-top: 1px solid rgba(0, 0, 0, 0.6); + } + + &__PanelHeader { + border-bottom: 1px solid rgba(0, 0, 0, 0.6); + + height: 63px; + width: 100%; + font-size: 16px; + color: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + gap: 16px; + cursor: pointer; + } + + &__StepNumber { + border-radius: 50%; + display: flex; + border: 1px solid rgba(0, 0, 0, 0.4); + width: 30px; + height: 30px; + align-items: center; + justify-content: center; + font-weight: bold; + } + + &__Content { + display: none; + padding: 15px 0 30px 48px; + + .formAccordionDigi__PanelHeader { + border-top: 0; + } + + &.active { + display: block; + } + + & > .formAccordionDigi { + margin-top: -15px; + + .formAccordionDigi__Content { + padding-left: 32px; + } + + .formAccordionDigi__Panel { + &:last-child { + .formAccordionDigi__PanelHeader { + border-bottom: 0; + } + } + } + } + } + + .validationIcon { + height: 16px; + width: 16px; + } + + .validationIcon--invalid { + fill: #ff9f00; + } + + &__Panel { + &.active { + display: block; + + > .formAccordionDigi__PanelHeader { + border-bottom: 0; + + .formAccordionDigiTitle { + font-weight: bold; + } + + .formAccordionDigi__StepNumber { + border-color: var(--skin-color-primary, #cc1e1c); + background: var(--skin-color-primary, #cc1e1c); + color: var(--text-color-contrast-switch, #fff); + } + } + + + .formAccordionDigi__Panel { + border-top: 1px solid rgba(0, 0, 0, 0.6); + } + } + + &:last-child { + .formAccordionDigi__Panel { + &:last-child { + border-bottom: 1px solid rgba(0, 0, 0, 0.6); + } + } + } + } +} diff --git a/src/extensions/components/registration/FormAccordion/index.tsx b/src/extensions/components/registration/FormAccordion/index.tsx new file mode 100644 index 000000000..66748be9a --- /dev/null +++ b/src/extensions/components/registration/FormAccordion/index.tsx @@ -0,0 +1,222 @@ +import { FieldContext } from 'rc-field-form'; +import { FormInstance } from 'rc-field-form'; +import React, { useCallback, useState, Children, useRef } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; +import { InvalidIcon } from '../../../../resources/img/icons'; +import { + Button, + ButtonItem, + BUTTON_TYPES +} from '../../../../components/button/Button'; +import { useTranslation } from 'react-i18next'; +import './formAccordion.styles.scss'; + +interface FormAccordionProps { + enableAutoScroll?: boolean; + children: React.ReactChild | React.ReactChild[]; + onComplete?: () => void; +} + +const scrollOffset = 80; + +interface FormAccordionItemProps { + disableNextButton?: boolean; + form?: FormInstance; + stepNumber?: number; + tabIndex?: number; + title: string; + subTitle?: string; + formFields?: string[]; + errorOnTouchExtraFields?: string[]; + children: React.ReactChild | React.ReactChild[]; +} + +export const FormAccordion = ({ + children, + enableAutoScroll, + onComplete +}: FormAccordionProps) => { + const [activePanel, setActivePanel] = useState(0); + const ref = useRef(null); + + const onClickNext = useCallback(() => { + if (activePanel < Children.count(children) - 1) { + setActivePanel(activePanel + 1); + } else { + setActivePanel(null); + onComplete?.(); + } + }, [activePanel, children, onComplete]); + + const handlePanelClick = useCallback( + (panel: number, isTabPressed: boolean) => { + setActivePanel( + activePanel === panel && !isTabPressed ? null : panel + ); + if (enableAutoScroll) { + const element = document.getElementById(`panel-${panel}`); + const offsetPosition = + element.getBoundingClientRect().top + + window.pageYOffset - + scrollOffset; + + window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); + } + }, + [activePanel, enableAutoScroll] + ); + const debouncedHandlePanelClick = useDebouncedCallback( + handlePanelClick, + 200, + { leading: true, trailing: false } + ); + + return ( +
+ {Children.toArray(children).map((child, index) => { + if ( + (child as JSX.Element).type.displayName !== + 'FormAccordionItem' && + process.env.NODE_ENV === 'development' + ) { + console.warn('FormAccordionItem expected'); + } + + return ( + + ); + })} +
+ ); +}; +FormAccordion.displayName = 'FormAccordion'; + +export const FormAccordionPanel = ({ + stepNumber, + title, + subTitle, + formFields = [], + errorOnTouchExtraFields = [], + children, + handlePanelClick, + handleNextStep, + isActive, + index, + form, + disableNextButton, + tabIndex +}: FormAccordionItemProps & { + index: number; + isActive: boolean; + handlePanelClick: ( + index: number, + focusFirstElement?: boolean, + onlyClose?: boolean + ) => void; + handleNextStep: () => void; +}) => { + const [id] = useState((Math.random() + 1).toString(36).substring(7)); + const formContext = React.useContext(FieldContext); + const fieldsToCheck = [...formFields, ...errorOnTouchExtraFields]; + const isFieldsInValid = (form || formContext) + .getFieldsError(formFields) + .some((error) => error.errors.length !== 0); + + const isValid = !( + formContext.isFieldsTouched(fieldsToCheck) && isFieldsInValid + ); + + const { t: translate } = useTranslation(); + + const buttonAnswerVideoCall: ButtonItem = { + title: translate('registration.accordion.item.continueButton.title'), + label: translate('registration.accordion.item.continueButton.label'), + type: BUTTON_TYPES.LINK + }; + + const childrenWithProps = Children.map(children, (child) => { + // With this code we can have multi accordion levels and have a next button + if ((child as JSX.Element)?.type?.displayName === 'FormAccordion') { + return ( + child && + React.cloneElement(child as React.ReactElement, { + onComplete: handleNextStep + }) + ); + } else { + return child; + } + }); + + return ( +
+
e.code === 'Space' && handlePanelClick(index)} + onFocus={(ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + handlePanelClick(index, true); + }} + onClick={() => handlePanelClick(index)} + aria-controls={`content-${id}`} + aria-expanded={isActive} + tabIndex={0} + > + {stepNumber && ( +
+ {stepNumber} +
+ )} +
{title}
+ {subTitle && ( +
{subTitle}
+ )} + {!isValid && ( +
+ +
+ )} +
+
+ {childrenWithProps} + + {!disableNextButton && ( +
+
+ ); +}; + +export const FormAccordionItem = ({ children }: FormAccordionItemProps) => ( +
{children}
+); + +FormAccordionItem.displayName = 'FormAccordionItem'; + +FormAccordion.Item = FormAccordionItem; diff --git a/src/extensions/components/registration/InputFormField/index.tsx b/src/extensions/components/registration/InputFormField/index.tsx new file mode 100644 index 000000000..1fb3b680e --- /dev/null +++ b/src/extensions/components/registration/InputFormField/index.tsx @@ -0,0 +1,46 @@ +import { Field } from 'rc-field-form'; +import React from 'react'; + +interface InputProps { + name?: string; + min?: number; + max?: number; + placeholder?: string; + type?: string; + pattern?: RegExp; + tabIndex?: number; + autoFocus?: boolean; +} + +const LocalInput = ({ + value, + type, + ...rest +}: { + type?: string; + value?: string; + tabIndex?: number; + placeholder?: string; +}) => ( + +); + +export const InputFormField = ({ + type = 'text', + name, + placeholder, + pattern, + ...rest +}: InputProps) => { + const patternRules = pattern ? { pattern } : {}; + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/PasswordFormField/index.tsx b/src/extensions/components/registration/PasswordFormField/index.tsx new file mode 100644 index 000000000..dd3c15bbb --- /dev/null +++ b/src/extensions/components/registration/PasswordFormField/index.tsx @@ -0,0 +1,31 @@ +import { Field } from 'rc-field-form'; +import React from 'react'; +import { VALIDITY_VALID } from '../../../../components/registration/registrationHelpers'; +import { RegistrationPassword } from '../../../../components/registration/RegistrationPassword'; + +const LocalPassword = ({ + onChange +}: { + value?: string; + onChange?: (value: string) => void; +}) => { + const [password, setPassword] = React.useState(); + return ( + setPassword(password)} + onValidityChange={(validity) => + validity === VALIDITY_VALID && onChange(password) + } + passwordNote="" + onKeyDown={() => null} + /> + ); +}; + +export const PasswordFormField = () => { + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/RadioBoxGroup/index.tsx b/src/extensions/components/registration/RadioBoxGroup/index.tsx new file mode 100644 index 000000000..1ae8f54f7 --- /dev/null +++ b/src/extensions/components/registration/RadioBoxGroup/index.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Field } from 'rc-field-form'; +import { + RadioButton, + RadioButtonItem +} from '../../../../components/radioButton/RadioButton'; +import { NamePath } from 'rc-field-form/es/interface'; + +interface RadioBoxGroupProps { + name: string | number | any; + options: Array<{ label: string; value: string }>; + preset?: string; +} + +const RadioBox = ({ + last, + value, + onChange, + valueRadio, + ...item +}: { + value?: string; + onChange?: (value: string) => void; + valueRadio: string; + last?: boolean; +} & Omit< + RadioButtonItem, + 'handleRadioButton' | 'checked' | 'type' | 'value' | 'inputId' +>) => { + const inputId = `radio-${valueRadio}`; + return ( + + ); +}; + +const LocalRadioBox = ({ + options, + ...rest +}: RadioBoxGroupProps & { + value?: string; + onChange?: (value: string) => void; +}) => { + return ( + <> + {options.map(({ value, label }, i) => ( + + ))} + + ); +}; +export const RadioBoxGroup = ({ + name, + dependencies, + ...props +}: RadioBoxGroupProps & { dependencies?: NamePath[] }) => { + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/RegistrationForm.tsx b/src/extensions/components/registration/RegistrationForm.tsx new file mode 100644 index 000000000..aa66b9fb0 --- /dev/null +++ b/src/extensions/components/registration/RegistrationForm.tsx @@ -0,0 +1,523 @@ +import * as React from 'react'; +import { + Button, + ButtonItem, + BUTTON_TYPES +} from '../../../components/button/Button'; +import { + AgencyDataInterface, + ConsultantDataInterface, + ConsultingTypeInterface, + TenantContext +} from '../../../globalState'; +import Form from 'rc-field-form'; +import './registrationForm.styles.scss'; +import { FormAccordion } from './FormAccordion'; +import { apiGetTopicsData } from '../../../api/apiGetTopicsData'; +import { TopicsDataInterface } from '../../../globalState/interfaces/TopicsDataInterface'; +import { CheckboxGroupFormField } from './CheckboxGroupFormField'; +import { RadioBoxGroup } from './RadioBoxGroup'; +import { PasswordFormField } from './PasswordFormField'; +import { apiPostRegistration } from '../../../api'; +import { FETCH_ERRORS, X_REASON } from '../../../api'; +import { UsernameFormField } from './UsernameFormField'; +import { AgencySelectionFormField } from './AgencyFields'; +import { InputFormField } from './InputFormField'; +import { CheckboxFormField } from './CheckboxFormField'; +import { RegistrationSuccessOverlay } from './RegistrationSuccessOverlay'; +import { AgencyInfo } from '../../../components/agencySelection/AgencyInfo'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useAppConfig } from '../../../hooks/useAppConfig'; +import { getTenantSettings } from '../../../utils/tenantSettingsHelper'; +import { budibaseLogout } from '../../../components/budibase/budibaseLogout'; +import { LegalLinksContext } from '../../../globalState/provider/LegalLinksProvider'; +import { useTranslation } from 'react-i18next'; +import { endpoints } from '../../../resources/scripts/endpoints'; +import { isNumber } from '../../../utils/isNumber'; +import { useLocation } from 'react-router-dom'; + +enum CounsellingRelation { + Self = 'SELF_COUNSELLING', + Relative = 'RELATIVE_COUNSELLING', + Parental = 'PARENTAL_COUNSELLING' +} + +enum Gender { + Male = 'MALE', + Female = 'FEMALE', + Diverse = 'DIVERSE', + NotProvided = 'NOT_PROVIDED' +} + +interface RegistrationFormProps { + consultingType?: ConsultingTypeInterface; + agency?: AgencyDataInterface; + consultant?: ConsultantDataInterface; + topic?: TopicsDataInterface; +} + +export const RegistrationForm = ({ + agency: preselectedAgency, + consultingType, + consultant, + topic +}: RegistrationFormProps) => { + const { tenant } = useContext(TenantContext); + const settings = useAppConfig(); + const [form] = Form.useForm(); + + const [topics, setTopics] = useState([] as TopicsDataInterface[]); + const [formErrors, setFormErrors] = useState([]); // This needs to be an array to trigger the changes on accordion + const [registrationWithSuccess, setRegistrationWithSuccess] = + useState(false); + const [isUsernameAlreadyInUse, setIsUsernameAlreadyInUse] = useState(false); + const { featureToolsEnabled } = getTenantSettings(); + const { t: translate } = useTranslation(); + const legalLinks = useContext(LegalLinksContext); + + const [currentValues, setValues] = useState({ + 'age': '', + 'agencyId': + consultant?.agencies?.length === 1 + ? consultant.agencies[0].id + : isNumber(`${preselectedAgency?.id}`) + ? preselectedAgency?.id + : '', + 'username': '', + 'consultingTypeId': + consultant?.agencies?.length === 1 + ? consultant.agencies[0].consultingType + : preselectedAgency?.consultingType || '', + 'postCode': '', + 'topicIds[]': [] + } as any); + + // Logout from budibase + useEffect(() => { + featureToolsEnabled && budibaseLogout(); + }, [featureToolsEnabled]); + + // When some that changes we check if the form is valid to enable/disable the submit button + useEffect(() => { + form.validateFields() + .then(() => setFormErrors([])) + .catch(({ errorFields }) => setFormErrors([...errorFields])); + }, [currentValues, form]); + + // Request the topics data + useEffect(() => { + (async () => { + const topics = await apiGetTopicsData(); + setTopics(topics); + if (!topic || !topics.find((t) => t.id === topic.id)) return; + + form.setFieldValue('mainTopicId', topic.id); + form.setFieldValue('topicIds[]', [topic.id]); + setValues((v) => ({ + ...v, + 'topicIds[]': [topic.id], + 'mainTopicId': topic.id + })); + })(); + }, [form, topic]); + + // Request the topics data + useEffect(() => { + if ( + currentValues['topicIds[]']?.length === 1 && + currentValues.mainTopicId !== currentValues['topicIds[]'][0] + ) { + form.setFieldValue('mainTopicId', currentValues['topicIds[]'][0]); + setValues({ + ...currentValues, + mainTopicId: currentValues['topicIds[]'][0] + }); + } + }, [topics, currentValues, form]); + + const useQuery = () => { + const { search } = useLocation(); + return useMemo(() => new URLSearchParams(search), [search]); + }; + const urlQuery: URLSearchParams = useQuery(); + + // Only max. 8 alphanumeric characters are allowed in the ref parameter + const getValidRef = (ref: string) => + ref.replace(/[^a-zA-Z0-9]/g, '').substring(0, 8); + + // Get the counselling relation from the query parameter + const getCounsellingRelation = (): string | null => { + const queryRelation = urlQuery.get('counsellingRelation'); + + if (!queryRelation) return null; + + const fullRelation = `${queryRelation.toUpperCase()}_COUNSELLING`; + const allRelations: string[] = Object.values(CounsellingRelation); + + if (allRelations.includes(fullRelation)) { + return fullRelation; + } + + return null; + }; + + // When the form is submitted we send the data to the API + const onSubmit = useCallback( + (formValues) => { + const finalValues = { + username: formValues.username, + password: encodeURIComponent(formValues.password), + agencyId: formValues.agencyId?.toString(), + mainTopicId: formValues.mainTopicId?.toString(), + postcode: formValues.postCode, + termsAccepted: formValues.termsAccepted, + gender: formValues.gender, + age: Number(formValues.age), + topicIds: formValues['topicIds[]'].map(Number), + counsellingRelation: formValues.counsellingRelation, + consultingType: formValues.consultingTypeId, + ...(consultant && { consultantId: consultant.consultantId }), + referer: urlQuery.get('ref') + ? getValidRef(urlQuery.get('ref')) + : null + }; + apiPostRegistration( + endpoints.registerAsker, + finalValues, + settings.multitenancyWithSingleDomainEnabled, + tenant + ) + .then(() => setRegistrationWithSuccess(true)) + .catch((errorRes) => { + if ( + errorRes.status === 409 && + errorRes.headers?.get(FETCH_ERRORS.X_REASON) === + X_REASON.USERNAME_NOT_AVAILABLE + ) { + form.setFields([ + { + name: 'username', + errors: ['Username already in use'] + } + ]); + setIsUsernameAlreadyInUse(true); + } + }); + }, + [consultant, form, settings, tenant, urlQuery] + ); + + // When some topic id is selected we need to change the list of main topics + const mainTopicOptions = useMemo( + () => + topics + ?.filter((topic) => + (currentValues['topicIds[]'] || []).includes(topic.id) + ) + .map(({ id, name }) => ({ label: name, value: id + '' })), + [currentValues, topics] + ); + + const buttonItemSubmit: ButtonItem = { + label: translate('registration.submitButton.label'), + type: BUTTON_TYPES.PRIMARY + }; + + return ( + <> +
+ setValues({ ...currentValues, ...changedValues }) + } + initialValues={currentValues} + form={form} + validateTrigger={['onBlur', 'onChange']} + > +

+ {translate('registrationDigi.headline')} +

+ {consultant && ( +

{translate('registrationDigi.teaser.consultant')}

+ )} + + + + + +
+ +
+ +
+ {translate( + 'registrationDigi.age.label' + )} +
+
+
+ +
+ + + ({ + label: translate( + `registrationDigi.gender.options.${value.toLowerCase()}` + ), + value + }) + )} + /> +
+
+ + + ({ + label: translate( + `registrationDigi.counsellingRelation.options.${value.toLowerCase()}` + ), + value + }))} + preset={getCounsellingRelation()} + /> + + + +
+ {topics?.map((topic) => ( +
+ + +
+ ))} +
+
+ + + + {mainTopicOptions.length === 0 && ( +

+ {translate( + 'registrationDigi.mainTopics.selectAtLestOneTopic' + )} +

+ )} +
+
+
+ + + + + + + + + + + + + + + +
+ +
+ legalLink.registration) + .map( + (legalLink, index, { length }) => + (index > 0 + ? index < length - 1 + ? ', ' + : translate( + 'registration.dataProtection.label.and' + ) + : '') + + `${translate(legalLink.label)}` + ) + .join(''), + translate( + 'registration.dataProtection.label.suffix' + ) + ].join(' ')} + name="termsAccepted" + localValue="true" + /> +
+ +