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/package-lock.json b/package-lock.json index 59c3a0e09..e611ddb55 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": { "@calcom/embed-snippet": "^1.0.1", "@draft-js-plugins/buttons": "^4.3.2", @@ -58,6 +58,7 @@ "mini-css-extract-plugin": "^2.7.7", "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", @@ -5815,6 +5816,11 @@ "dev": true, "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", "dev": true, @@ -19291,6 +19297,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, @@ -24228,6 +24264,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "src/extensions": {} + "src/extensions": { + "name": "@onlineberatung/onlineberatung-frontend-extensions", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "rc-field-form": "^1.41.0" + } + } } } diff --git a/package.json b/package.json index ab7b6d9ed..f25f31dbf 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" 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/components/E2EEncryptionSupportBanner/E2EEncryptionSupportBanner.tsx b/src/components/E2EEncryptionSupportBanner/E2EEncryptionSupportBanner.tsx index 88151a29f..023bf653a 100644 --- a/src/components/E2EEncryptionSupportBanner/E2EEncryptionSupportBanner.tsx +++ b/src/components/E2EEncryptionSupportBanner/E2EEncryptionSupportBanner.tsx @@ -15,7 +15,6 @@ import { UserDataContext } from '../../globalState'; import { STATUS_EMPTY } from '../../globalState/interfaces'; -import { Link } from 'react-router-dom'; export const E2EEncryptionSupportBanner = () => { const [showBanner, setShowBanner] = useState(false); @@ -25,18 +24,22 @@ export const E2EEncryptionSupportBanner = () => { const { sessions } = useContext(SessionsDataContext); useEffect(() => { + const isFirefox = + navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + if ( - hasVideoCallAbility(userData, consultingTypes) && - // don't show banner when user enters first message - !( - hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && - (sessions.length === 0 || - (sessions.length === 1 && - sessions[0]?.session?.status === STATUS_EMPTY)) - ) + isFirefox || + (hasVideoCallAbility(userData, consultingTypes) && + // don't show banner when user enters first message + !( + hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && + (sessions.length === 0 || + (sessions.length === 1 && + sessions[0]?.session?.status === STATUS_EMPTY)) + )) ) { setShowBanner( - !supportsE2EEncryptionVideoCall() && + (isFirefox || !supportsE2EEncryptionVideoCall()) && !sessionStorage.getItem('hideEncryptionBanner') ); } @@ -71,12 +74,7 @@ export const E2EEncryptionSupportBanner = () => { fill="black" /> -

- {translate('help.videoCall.banner.content')}{' '} - - {translate('help.videoCall.banner.more')} - -

+

{translate('help.videoCall.banner.content')}

); }; diff --git a/src/components/termsandconditions/TermsAndConditions.tsx b/src/components/termsandconditions/TermsAndConditions.tsx index 64f957c77..3cb98d61d 100644 --- a/src/components/termsandconditions/TermsAndConditions.tsx +++ b/src/components/termsandconditions/TermsAndConditions.tsx @@ -42,20 +42,23 @@ export const TermsAndConditions = () => { buttons: [] }); - const transformText2Link = (text: string) => { - let termsLabel = translate( - 'termsAndConditionOverlay.labels.termsAndCondition' - ); - let privacyLabel = translate('termsAndConditionOverlay.labels.privacy'); - text = text.replace( - termsLabel, - `${termsLabel}` - ); - return text.replace( - privacyLabel, - `${privacyLabel}` - ); - }; + // The Terms and Conditions are currently not used in this project. + // To prevent the app from showing a popup, the following code is commented out. + + // const transformText2Link = (text: string) => { + // let termsLabel = translate( + // 'termsAndConditionOverlay.labels.termsAndCondition' + // ); + // let privacyLabel = translate('termsAndConditionOverlay.labels.privacy'); + // text = text.replace( + // termsLabel, + // `${termsLabel}` + // ); + // return text.replace( + // privacyLabel, + // `${privacyLabel}` + // ); + // }; const transformText2DataPrivacyLink = (text: string) => { let hereLabel = translate('termsAndConditionOverlay.labels.here'); @@ -65,20 +68,20 @@ export const TermsAndConditions = () => { ); }; - const standardButtons = (userConfirmed: boolean) => { - return [ - { - label: translate('termsAndConditionOverlay.buttons.decline'), - function: OVERLAY_FUNCTIONS.CLOSE, - type: BUTTON_TYPES.SECONDARY - }, - { - label: translate('termsAndConditionOverlay.buttons.accept'), - disabled: !userConfirmed, - type: BUTTON_TYPES.PRIMARY - } - ]; - }; + // const standardButtons = (userConfirmed: boolean) => { + // return [ + // { + // label: translate('termsAndConditionOverlay.buttons.decline'), + // function: OVERLAY_FUNCTIONS.CLOSE, + // type: BUTTON_TYPES.SECONDARY + // }, + // { + // label: translate('termsAndConditionOverlay.buttons.accept'), + // disabled: !userConfirmed, + // type: BUTTON_TYPES.PRIMARY + // } + // ]; + // }; const dataPrivacyButtons = [ { @@ -88,52 +91,51 @@ export const TermsAndConditions = () => { ]; useEffect(() => { - if ( - hasChanged( - tenantData, - userData, - 'termsAndConditionsConfirmation' - ) && - hasChanged(tenantData, userData, 'dataPrivacyConfirmation') - ) { - setViewState({ - headlineText: translate( - 'termsAndConditionOverlay.title.termsAndConditionAndPrivacy' - ), - mainText: translate( - 'termsAndConditionOverlay.contentLine1.termsAndConditionAndPrivacy' - ), - checkboxText: transformText2Link( - translate( - 'termsAndConditionOverlay.contentLine2.termsAndConditionAndPrivacy' - ) - ), - showOverlay: true, - userConfirmed: viewState.userConfirmed, - buttons: standardButtons(viewState.userConfirmed) - }); - } else if ( - hasChanged(tenantData, userData, 'termsAndConditionsConfirmation') - ) { - setViewState({ - headlineText: translate( - 'termsAndConditionOverlay.title.termsAndCondition' - ), - mainText: translate( - 'termsAndConditionOverlay.contentLine1.termsAndCondition' - ), - checkboxText: transformText2Link( - translate( - 'termsAndConditionOverlay.contentLine2.termsAndCondition' - ) - ), - showOverlay: true, - userConfirmed: viewState.userConfirmed, - buttons: standardButtons(viewState.userConfirmed) - }); - } else if ( - hasChanged(tenantData, userData, 'dataPrivacyConfirmation') - ) { + // if ( + // hasChanged( + // tenantData, + // userData, + // 'termsAndConditionsConfirmation' + // ) && + // hasChanged(tenantData, userData, 'dataPrivacyConfirmation') + // ) { + // setViewState({ + // headlineText: translate( + // 'termsAndConditionOverlay.title.termsAndConditionAndPrivacy' + // ), + // mainText: translate( + // 'termsAndConditionOverlay.contentLine1.termsAndConditionAndPrivacy' + // ), + // checkboxText: transformText2Link( + // translate( + // 'termsAndConditionOverlay.contentLine2.termsAndConditionAndPrivacy' + // ) + // ), + // showOverlay: true, + // userConfirmed: viewState.userConfirmed, + // buttons: standardButtons(viewState.userConfirmed) + // }); + // } else if ( + // hasChanged(tenantData, userData, 'termsAndConditionsConfirmation') + // ) { + // setViewState({ + // headlineText: translate( + // 'termsAndConditionOverlay.title.termsAndCondition' + // ), + // mainText: translate( + // 'termsAndConditionOverlay.contentLine1.termsAndCondition' + // ), + // checkboxText: transformText2Link( + // translate( + // 'termsAndConditionOverlay.contentLine2.termsAndCondition' + // ) + // ), + // showOverlay: true, + // userConfirmed: viewState.userConfirmed, + // buttons: standardButtons(viewState.userConfirmed) + // }); + // } else + if (hasChanged(tenantData, userData, 'dataPrivacyConfirmation')) { setViewState({ headlineText: translate( 'termsAndConditionOverlay.title.privacy' @@ -141,7 +143,9 @@ export const TermsAndConditions = () => { mainText: transformText2DataPrivacyLink( translate('termsAndConditionOverlay.contentLine1.privacy') ), - checkboxText: null, + checkboxText: translate( + 'termsAndConditionOverlay.contentLine2.privacy' + ), showOverlay: true, userConfirmed: viewState.userConfirmed, buttons: dataPrivacyButtons diff --git a/src/extensions/components/askerInfo/AskerInfoContent.tsx b/src/extensions/components/askerInfo/AskerInfoContent.tsx new file mode 100644 index 000000000..08921e8f1 --- /dev/null +++ b/src/extensions/components/askerInfo/AskerInfoContent.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { SESSION_LIST_TYPES } from '../../../components/session/sessionHelpers'; +import { + AUTHORITIES, + hasUserAuthority, + SessionTypeContext, + TenantContext, + UserDataContext, + ActiveSessionContext +} from '../../../globalState'; +import { + ConsultingSessionDataInterface, + TopicSessionInterface +} from '../../../globalState/interfaces'; +import { AskerInfoAssign } from '../../../components/askerInfo/AskerInfoAssign'; +import '../../../components/askerInfo/askerInfo.styles'; +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..d7a9f1642 --- /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'; +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..4e61f327e --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/Agency/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { AgencyDataInterface } from '../../../../../globalState/interfaces'; +import { InfoTooltip } from '../../../../../components/infoTooltip/InfoTooltip'; +import { RadioButton } from '../../../../../components/radioButton/RadioButton'; +import { AgencyLanguages } from '../../../../../components/agencyRadioSelect/AgencyLanguages'; + +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()} + > + {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..eafd6d9dc --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/AgencySelection/index.tsx @@ -0,0 +1,262 @@ +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/interfaces'; +import { PinIcon } from '../../../../../resources/img/icons'; +import { VALID_POSTCODE_LENGTH } from '../../../../../components/agencySelection/agencySelectionHelpers'; +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'; +import { AgencyRadioSelect } from '../../../../../components/agencyRadioSelect/AgencyRadioSelect'; + +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 === 0 && + 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) => ( +
  • + +
  • + ))} +
+
+
+ + + + + + {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..4e64f104c --- /dev/null +++ b/src/extensions/components/registration/AgencyFields/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { FieldContext } from 'rc-field-form'; +import { + ConsultingTypeBasicInterface, + AgencyDataInterface +} from '../../../../globalState/interfaces'; +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; +} + +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..275267e15 --- /dev/null +++ b/src/extensions/components/registration/CheckboxFormField/index.tsx @@ -0,0 +1,59 @@ +import { Field } from 'rc-field-form'; +import * as React from 'react'; +import { + Checkbox, + CheckboxItem +} from '../../../../components/checkbox/Checkbox'; +import { FC } from 'react'; + +export interface CheckboxFormFieldProps + extends Omit< + CheckboxItem, + 'labelId' | 'inputId' | 'checkboxHandle' | 'checked' + > { + id?: string; + labelClass?: string; + localValue: string; + onChange?: (v: string) => void; +} + +const CheckBoxLocal: FC = ({ + value, + onChange, + localValue, + id, + name, + ...checkboxProps +}) => { + const onLocalChange = React.useCallback( + () => onChange(value === localValue ? '' : localValue), + [onChange, value, localValue] + ); + + return ( + e.key === 'Space' && onLocalChange()} + {...checkboxProps} + /> + ); +}; + +export const CheckboxFormField: FC = ({ + name, + children, + ...props +}) => { + return ( + + + {children} + + + ); +}; diff --git a/src/extensions/components/registration/CheckboxGroupFormField/index.tsx b/src/extensions/components/registration/CheckboxGroupFormField/index.tsx new file mode 100644 index 000000000..093a8771b --- /dev/null +++ b/src/extensions/components/registration/CheckboxGroupFormField/index.tsx @@ -0,0 +1,51 @@ +import { Field } from 'rc-field-form'; +import * as React from 'react'; +import { Checkbox } 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, '-')}`; + + return ( + e.key === 'Space' && onLocalChange()} + /> + ); +}; + +export const CheckboxGroupFormField = (props: CheckboxFormFieldProps) => { + return ( + + + + ); +}; diff --git a/src/extensions/components/registration/FormAccordion/FormAccordion.tsx b/src/extensions/components/registration/FormAccordion/FormAccordion.tsx new file mode 100644 index 000000000..a2604a48d --- /dev/null +++ b/src/extensions/components/registration/FormAccordion/FormAccordion.tsx @@ -0,0 +1,97 @@ +import React, { + useCallback, + useState, + useRef, + ReactElement, + useMemo +} from 'react'; +import { useDebouncedCallback } from 'use-debounce'; +import './formAccordion.styles.scss'; +import { FormAccordionItemProps } from './FormAccordionItem'; +import { DebouncedState } from 'use-debounce/lib/useDebouncedCallback'; + +export interface FormAccordionChildProps { + activePanel: FormAccordionItemProps['id']; + handlePanelClick: DebouncedState< + ( + panel: FormAccordionChildProps['activePanel'], + isTabPressed?: boolean + ) => void + >; + handleNextStep: () => void; +} + +interface FormAccordionProps { + children: ( + props: FormAccordionChildProps + ) => ReactElement[]; + onComplete?: () => void; +} + +const scrollOffset = 80; + +export const FormAccordion = ({ children, onComplete }: FormAccordionProps) => { + const childIds = useMemo( + () => + children({} as FormAccordionChildProps).map( + (child) => child.props.id + ), + [children] + ); + const [activePanel, setActivePanel] = useState< + FormAccordionChildProps['activePanel'] + >(childIds[0]); + const ref = useRef(null); + + const handleScroll = useCallback((panel) => { + setTimeout(() => { + const scrollContainer = + document.getElementsByClassName(`stageLayout`)[0]; + const element = document.getElementById(`panel-${panel}`); + const offsetPosition = element.offsetTop - scrollOffset; + + scrollContainer.scrollTo({ + top: offsetPosition, + behavior: 'smooth' + }); + }, 50); + }, []); + + const handleNextStep = useCallback(() => { + const childIdIndex = childIds.indexOf(activePanel); + setActivePanel(childIds?.[childIdIndex + 1]); + // Call onComplete when next on last panel was clicked + if (childIdIndex !== childIds.length - 1) { + return handleScroll(childIds?.[childIdIndex + 1]); + } + onComplete?.(); + }, [activePanel, childIds, handleScroll, onComplete]); + + const handlePanelClick = useCallback( + ( + panel: FormAccordionChildProps['activePanel'], + isTabPressed?: boolean + ) => { + setActivePanel( + activePanel === panel && !isTabPressed ? undefined : panel + ); + handleScroll(panel); + }, + [activePanel, handleScroll] + ); + const debouncedHandlePanelClick = useDebouncedCallback( + handlePanelClick, + 200, + { leading: true, trailing: false } + ); + + return ( +
+ {children({ + activePanel, + handlePanelClick: debouncedHandlePanelClick, + handleNextStep + })} +
+ ); +}; diff --git a/src/extensions/components/registration/FormAccordion/FormAccordionItem.tsx b/src/extensions/components/registration/FormAccordion/FormAccordionItem.tsx new file mode 100644 index 000000000..713c477ae --- /dev/null +++ b/src/extensions/components/registration/FormAccordion/FormAccordionItem.tsx @@ -0,0 +1,118 @@ +import { FieldContext } from 'rc-field-form'; +import React, { FC } from 'react'; +import './formAccordion.styles.scss'; +import { useTranslation } from 'react-i18next'; +import { + Button, + BUTTON_TYPES, + ButtonItem +} from '../../../../components/button/Button'; +import { InvalidIcon } from '../../../../resources/img/icons'; +import { FormAccordionChildProps } from './FormAccordion'; +import classNames from 'classnames'; + +export interface FormAccordionItemProps { + id?: string; + disableNextButton?: boolean; + stepNumber?: number; + title: string; + subTitle?: string; + formFields?: string[]; + errorOnTouchExtraFields?: string[]; +} + +export const FormAccordionItem: FC< + FormAccordionItemProps & FormAccordionChildProps +> = ({ + id, + stepNumber, + title, + subTitle, + formFields = [], + errorOnTouchExtraFields = [], + children, + handlePanelClick, + handleNextStep, + activePanel, + disableNextButton +}) => { + const formContext = React.useContext(FieldContext); + const fieldsToCheck = [...formFields, ...errorOnTouchExtraFields]; + + const isFieldsInValid = 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 isActive = activePanel === id; + + return ( +
+
e.code === 'Space' && handlePanelClick(id)} + onFocus={(ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + handlePanelClick(id, true); + }} + onClick={() => handlePanelClick(id)} + aria-controls={`content-${id}`} + aria-expanded={isActive} + tabIndex={0} + > + {!!stepNumber && ( +
+ {stepNumber} +
+ )} +
{title}
+ {subTitle && ( +
{subTitle}
+ )} + {!isValid && ( +
+ +
+ )} +
+
+ {children} + + {!disableNextButton && ( +
+
+ ); +}; 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/InputFormField/index.tsx b/src/extensions/components/registration/InputFormField/index.tsx new file mode 100644 index 000000000..ced178657 --- /dev/null +++ b/src/extensions/components/registration/InputFormField/index.tsx @@ -0,0 +1,44 @@ +import { Field } from 'rc-field-form'; +import React from 'react'; +import type { Rule } from 'rc-field-form/es/interface'; +import { FieldProps } from 'rc-field-form/es/Field'; + +interface InputProps { + name?: string; + min?: number; + max?: number; + placeholder?: string; + normalize?: FieldProps['normalize']; + type?: string; + rule?: Rule; + tabIndex?: number; + autoFocus?: boolean; +} + +export const InputFormField = ({ + type = 'text', + name, + placeholder, + normalize, + rule, + ...rest +}: InputProps) => { + return ( + + {({ value, ...props }) => ( + + )} + + ); +}; diff --git a/src/extensions/components/registration/PasswordFormField/index.tsx b/src/extensions/components/registration/PasswordFormField/index.tsx new file mode 100644 index 000000000..f3fa3b3d0 --- /dev/null +++ b/src/extensions/components/registration/PasswordFormField/index.tsx @@ -0,0 +1,30 @@ +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 +}: { + 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..028278352 --- /dev/null +++ b/src/extensions/components/registration/RadioBoxGroup/index.tsx @@ -0,0 +1,56 @@ +import React, { FC } from 'react'; +import { Field } from 'rc-field-form'; +import { RadioButton } from '../../../../components/radioButton/RadioButton'; +import { NamePath } from 'rc-field-form/es/interface'; +import classNames from 'classnames'; + +interface RadioBoxGroupProps { + name: string; + options: Array<{ label: string; value: string }>; + preset?: string; + dependencies?: NamePath[]; + normalize?: (value: string) => any; +} + +export const RadioBoxGroup: FC = ({ + name, + dependencies, + options, + preset, + normalize, + children, + ...props +}) => { + return ( + + {({ value, onChange }) => + options.map(({ value: valueRadio, label }, i) => { + const inputId = `radio-${valueRadio}`; + return ( + + {label} + + ); + }) + } + + ); +}; diff --git a/src/extensions/components/registration/RegistrationForm.tsx b/src/extensions/components/registration/RegistrationForm.tsx new file mode 100644 index 000000000..5fdfbfe34 --- /dev/null +++ b/src/extensions/components/registration/RegistrationForm.tsx @@ -0,0 +1,553 @@ +import * as React from 'react'; +import { + Button, + ButtonItem, + BUTTON_TYPES +} from '../../../components/button/Button'; +import { TenantContext } from '../../../globalState'; +import { TopicsDataInterface } from '../../../globalState/interfaces'; +import Form from 'rc-field-form'; +import './registrationForm.styles.scss'; +import { apiGetTopicsData } from '../../../api/apiGetTopicsData'; +import { CheckboxGroupFormField } from './CheckboxGroupFormField'; +import { RadioBoxGroup } from './RadioBoxGroup'; +import { PasswordFormField } from './PasswordFormField'; +import { apiPostRegistration, 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 { InfoTooltip } from '../../../components/infoTooltip/InfoTooltip'; +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 { useLocation } from 'react-router-dom'; +import LegalLinks from '../../../components/legalLinks/LegalLinks'; +import { FormAccordion } from './FormAccordion/FormAccordion'; +import { FormAccordionItem } from './FormAccordion/FormAccordionItem'; +import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; + +enum CounsellingRelation { + Self = 'SELF_COUNSELLING', + Relative = 'RELATIVE_COUNSELLING', + Parental = 'PARENTAL_COUNSELLING' +} + +enum Gender { + Male = 'MALE', + Female = 'FEMALE', + Diverse = 'DIVERSE', + NotProvided = 'NOT_PROVIDED' +} + +interface FormData { + 'age': string; + 'agencyId': number; + 'username': string; + 'password': string; + 'consultingTypeId': number; + 'postCode': string; + 'topicIds[]': number[]; + 'mainTopicId': number; + 'gender': Gender; + 'counsellingRelation': CounsellingRelation; +} + +export const RegistrationForm = () => { + const { tenant } = useContext(TenantContext); + const settings = useAppConfig(); + const [form] = Form.useForm(); + const { agency, consultingType, consultant, topic } = + useContext(UrlParamsContext); + + const [topics, setTopics] = useState([] as TopicsDataInterface[]); + const [valid, setValid] = useState(false); // 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 initialValues = useMemo(() => { + const agencyId = + consultant?.agencies?.length === 1 + ? consultant.agencies[0].id + : agency?.id || undefined; + + const consultingTypeId = + consultant?.agencies?.length === 1 + ? consultant.agencies[0].consultingType + : agency?.consultingType || undefined; + + return { + agencyId, + consultingTypeId, + 'topicIds[]': [] + } as FormData; + }, [agency?.consultingType, agency?.id, consultant?.agencies]); + + // Logout from budibase + useEffect(() => { + featureToolsEnabled && budibaseLogout(); + }, [featureToolsEnabled]); + + // When some that changes we check if the form is valid to enable/disable the submit button + + const store = Form.useWatch([], form); + useEffect(() => { + form.validateFields().then( + () => { + setValid(true); + }, + () => { + setValid(false); + } + ); + }, [store, 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]); + })(); + }, [form, topic]); + + const topicIds = Form.useWatch('topicIds[]', form); + const mainTopicId = Form.useWatch('mainTopicId', form); + useEffect(() => { + if (topicIds?.length > 0 && !topicIds.includes(mainTopicId)) { + form.setFieldValue('mainTopicId', topicIds[0]); + } + }, [mainTopicId, form, topicIds]); + + 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; + }; + + const preselectedAgencies = useMemo( + () => + agency + ? [agency] + : consultant?.agencies + ? consultant?.agencies + : [], + [agency, consultant?.agencies] + ); + + // 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) => (topicIds || []).includes(topic.id)) + .map(({ id, name }) => ({ label: name, value: id + '' })), + [topicIds, topics] + ); + + const buttonItemSubmit: ButtonItem = { + label: translate('registration.submitButton.label'), + type: BUTTON_TYPES.PRIMARY + }; + + return ( + <> +
+

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

+ {consultant && ( +

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

+ )} + + + {(props) => [ + + + {(props) => [ + +
+ +
+ { + if ( + !value.match( + /^\d{0,3}$/ + ) + ) { + return prevValue; + } + return value <= 100 + ? value + : 100; + }} + min={0} + max={100} + rule={{ + pattern: /^\d{0,3}$/, + max: 100, + min: 0 + }} + type="number" + /> +
+ {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) => ( +
+ + +
+ ))} +
+
, + + + parseInt(value) + } + options={mainTopicOptions} + /> + {mainTopicOptions.length === 0 && ( +

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

+ )} +
+ ]} +
+
, + + + , + + + {(props) => [ + + + , + + + + ]} + + + ]} +
+ +
+ + legalLink.registration} + legalLinks={legalLinks} + /> + +
+ +