diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 217645791..be54d30d4 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -23,18 +23,13 @@ describe('Login', () => { cy.get('#appRoot').should('exist'); }); - it.skip('displays the login at the root', () => { + it('displays the login at the root', () => { cy.visit('/'); cy.contains('Login'); cy.contains('Impressum'); cy.contains('Datenschutzerklärung'); }); - it('displays the consultingtype page at the root', () => { - cy.visit('/'); - cy.contains('Willkommen bei der Online-Beratung'); - }); - it('displays the login for resorts', () => { cy.visit('/suchtberatung'); cy.contains('Login'); diff --git a/cypress/e2e/registration/topic.cy.ts b/cypress/e2e/registration/topic.cy.ts index b4e22d317..311e6d288 100644 --- a/cypress/e2e/registration/topic.cy.ts +++ b/cypress/e2e/registration/topic.cy.ts @@ -19,7 +19,6 @@ let consultingTypes, agencies, topics; describe('Registration', () => { before(() => { - Cypress.env('TENANT_ENABLED', '1'); startWebSocketServer(); }); diff --git a/cypress/support/commands/api/agencies.ts b/cypress/support/commands/api/agencies.ts index 02b82c817..302df2295 100644 --- a/cypress/support/commands/api/agencies.ts +++ b/cypress/support/commands/api/agencies.ts @@ -14,11 +14,6 @@ const agenciesApi = (cy, getWillReturn, setWillReturn) => { cy.intercept('GET', `${endpoints.agencyServiceBase}*`, (req) => { const searchParams = new URL(req.url).searchParams; let agencies = getWillReturn('agencies'); - console.log( - agencies, - searchParams.get('topicId'), - searchParams.get('consultingType') - ); if (searchParams.has('topicId')) { agencies = agencies.filter((a) => a.topicIds?.includes(parseInt(searchParams.get('topicId'))) diff --git a/cypress/support/commands/helper/fastLogin.ts b/cypress/support/commands/helper/fastLogin.ts index 74efcba2a..f4e6dc9c6 100644 --- a/cypress/support/commands/helper/fastLogin.ts +++ b/cypress/support/commands/helper/fastLogin.ts @@ -54,8 +54,6 @@ const fastLoginCommand = (getWillReturn, setWillReturn) => cy.wait('@usersData'); cy.wait('@settings'); cy.wait('@consultingTypeServiceBaseBasic'); - cy.wait('@patchUsersData'); - cy.wait('@fetchMyKeys'); if (userId === USER_ASKER) { cy.wait('@askerSessions'); } else { diff --git a/cypress/support/commands/mockApi.ts b/cypress/support/commands/mockApi.ts index 4956be8af..30d2d8b40 100644 --- a/cypress/support/commands/mockApi.ts +++ b/cypress/support/commands/mockApi.ts @@ -1,30 +1,9 @@ import merge from 'lodash.merge'; -import { - generateAskerSession, - generateConsultantSession, - generateMessage, - generateMessagesReply, - sessionsReply -} from '../sessions'; +import { generateAskerSession, generateConsultantSession } from '../sessions'; import { endpoints } from '../../../src/resources/scripts/endpoints'; -import { - getAskerSessions, - setAskerSessions, - updateAskerSession -} from './helper/askerSessions'; -import { - getConsultantSessions, - setConsultantSessions, - updateConsultantSession -} from './helper/consultantSessions'; -import { deepMerge } from '../helpers'; -import { decodeUsername } from '../../../src/utils/encryptionHelpers'; -import { getMessages, setMessages } from './helper/messages'; -import { - SETTING_E2E_ENABLE, - SETTING_FILEUPLOAD_MAXFILESIZE, - SETTING_MESSAGE_MAXALLOWEDSIZE -} from '../../../src/api/apiRocketChatSettingsPublic'; +import { setAskerSessions } from './helper/askerSessions'; +import { setConsultantSessions } from './helper/consultantSessions'; +import { setMessages } from './helper/messages'; import { config } from '../../../src/resources/scripts/config'; import usersChatApi from './api/users/chat'; import usersConsultantsApi from './api/users/consultants'; diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 51ed4097d..23c5af1ad 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,6 +1,10 @@ import 'cypress-file-upload'; import './commands'; +before(() => { + Cypress.env('TENANT_ENABLED', '1'); +}); + beforeEach(() => { window.localStorage.setItem('locale', 'de'); window.localStorage.setItem('showDevTools', '0'); diff --git a/package.json b/package.json index 81592df3d..4ccba450d 100644 --- a/package.json +++ b/package.json @@ -192,19 +192,19 @@ "dev": "node scripts/start.js", "dev:server": "nodemon", "build": "node scripts/build.js", - "test": "cross-env FAST_REFRESH=false 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": "cross-env NODE_ENV=development FAST_REFRESH=false 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:components": "NODE_ENV=development cypress run --component --headed", - "test:build": "cross-env FAST_REFRESH=false 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\"", + "test:build": "cross-env NODE_ENV=production FAST_REFRESH=false 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\"", "release": "standard-version", "lint": "npm run lint:scripts && npm run lint:style", "lint:scripts": "eslint src && tsc", "lint:style": "stylelint src/**/*.scss", "lint:style:fix": "stylelint --fix src/**/*.scss", "cypress": "run-p cypress:*", - "cypress:start-cra": "NODE_ENV=production BROWSER=none PORT=9001 WDS_SOCKET_PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 npm run dev", + "cypress:start-cra": "NODE_ENV=development BROWSER=none PORT=9001 WDS_SOCKET_PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 npm run dev", "cypress:wait-and-open": "run-s cypress:wait-and-open:open cypress:wait-and-open:wait-on-cra", "cypress:wait-and-open:wait-on-cra": "wait-on http://127.0.0.1:9001", - "cypress:wait-and-open:open": "cross-env NODE_ENV=production PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 cypress open", + "cypress:wait-and-open:open": "cross-env NODE_ENV=development PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 cypress open", "dtsgen": "ts-node dtsgen.ts", "prepare": "husky install", "browserstack": "browserstack-cypress run --sync --env BROWSER=none,REACT_APP_API_URL=http://127.0.0.1:9001,HTTPS=0,PORT=9001,WDS_SOCKET_PORT=9001", diff --git a/src/api/apiAgencySelection.ts b/src/api/apiAgencySelection.ts index 8f7acc972..9c89ad7b6 100644 --- a/src/api/apiAgencySelection.ts +++ b/src/api/apiAgencySelection.ts @@ -9,7 +9,7 @@ export const apiAgencySelection = async ( ...params }: { postcode?: string; - consultingType?: number | undefined; + consultingType?: number; topicId?: number; age?: number; gender?: string; diff --git a/src/api/apiGetTopicGroups.ts b/src/api/apiGetTopicGroups.ts index 334c17c9c..87fe0bb33 100644 --- a/src/api/apiGetTopicGroups.ts +++ b/src/api/apiGetTopicGroups.ts @@ -1,4 +1,4 @@ -import { TopicGroup } from '../globalState/interfaces/TopicGroups'; +import { TopicGroup } from '../globalState/interfaces'; import { endpoints } from '../resources/scripts/endpoints'; import { fetchData, FETCH_ERRORS, FETCH_METHODS } from './fetchData'; @@ -7,7 +7,7 @@ export const apiGetTopicGroups = async (): Promise<{ }> => { return fetchData({ url: endpoints.topicGroups, - responseHandling: [FETCH_ERRORS.EMPTY], + responseHandling: [FETCH_ERRORS.EMPTY, FETCH_ERRORS.NO_MATCH], method: FETCH_METHODS.GET }); }; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 55a1e349d..e3838d680 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useState, useEffect, useRef } from 'react'; -import { TextField, Typography } from '@mui/material'; +import { InputBaseComponentProps, TextField, Typography } from '@mui/material'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; import { useTranslation } from 'react-i18next'; @@ -22,6 +22,7 @@ export interface InputProps { endAdornment?: JSX.Element; isValueValid?(value: string): Promise; inputType?: 'number' | 'tel' | 'text' | 'password'; + inputProps?: InputBaseComponentProps; info?: string; autoComplete?: string; errorMessage?: string; @@ -40,6 +41,7 @@ export const Input = ({ endAdornment, isValueValid, inputType, + inputProps, info, inputMode, errorMessage, @@ -143,19 +145,20 @@ export const Input = ({ label={label} autoComplete={autoComplete} inputProps={{ - inputMode: inputMode + inputMode: inputMode, + ...inputProps }} sx={{ '&[type=number]': { - '-moz-appearance': 'textfield' + MozAppearance: 'textfield' }, '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - 'margin': 0 + WebkitAppearance: 'none', + margin: 0 }, '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - 'margin': 0 + WebkitAppearance: 'none', + margin: 0 }, 'mt': '24px', '& legend': { @@ -205,7 +208,7 @@ export const Input = ({ setShrink(true); }} onBlur={handleBlur} - > + /> {info && !inputError && !showSuccessMessage && ( )} - {multipleCriteria && - multipleCriteria.map((criteria) => { - return ( - - {getMultipleCriteriaDesign(criteria).icon}{' '} - {t(criteria.info)} - - ); - })} + {multipleCriteria?.map((criteria) => ( + + {getMultipleCriteriaDesign(criteria).icon}{' '} + {t(criteria.info)} + + ))} ); }; diff --git a/src/containers/registration/hooks/useAgenciesForRegistration.ts b/src/containers/registration/hooks/useAgenciesForRegistration.ts index fbe24654c..918d428fa 100644 --- a/src/containers/registration/hooks/useAgenciesForRegistration.ts +++ b/src/containers/registration/hooks/useAgenciesForRegistration.ts @@ -13,7 +13,7 @@ import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvide import { VALID_POSTCODE_LENGTH } from '../../../components/agencySelection/agencySelectionHelpers'; interface AgenciesForRegistrationArgs { - consultingType: ConsultingTypeInterface; + consultingType?: ConsultingTypeInterface; postcode: string; topic: TopicsDataInterface; } @@ -59,7 +59,7 @@ export const useAgenciesForRegistration = ({ const allAgencies = useMemo(() => { // As long as no consulting type or topic is selected we can't show any agencies - if (!consultingType || topicsEnabledAndUnSelected) { + if (!consultingType && topicsEnabledAndUnSelected) { return []; } @@ -131,8 +131,9 @@ export const useAgenciesForRegistration = ({ apiAgencySelection( { ...(autoSelectPostcode ? {} : { postcode }), - consultingType: consultingType?.id, topicId: topic?.id, + // API will ignore consultingType if topicId isset but its required to be send + consultingType: topic?.id || consultingType?.id, fetchConsultingTypeDetails: true }, abortController.signal diff --git a/src/extensions/components/app/app.tsx b/src/extensions/components/app/app.tsx deleted file mode 100644 index 658d2cbd1..000000000 --- a/src/extensions/components/app/app.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; -import { ThemeProvider } from '@mui/material'; -import { UrlParamsProvider } from '../../../globalState/provider/UrlParamsProvider'; -import { RegistrationProvider } from '../../../globalState'; -import { App as CoreApp, AppProps } from '../../../components/app/app'; -import { lazy } from 'react'; -import '../../../resources/styles/mui-variables-mapping'; -import theme from '../../theme'; - -const Registration = lazy(() => - import('../registration/Registration').then((m) => ({ - default: m.Registration - })) -); - -const NewRegistration = () => ( - - - - - -); - -export const App = (props: AppProps) => { - // The login is possible both at the root URL as well as with an - // optional resort name. Since resort names are dynamic, we have - // to find out if the provided path is a resort name. If not, we - // use the authenticated app as a catch-all fallback. - - const extraRoutes = [ - ...props.extraRoutes, - { - route: { - path: [ - '/registration', - '/registration/topic-selection', - '/registration/zipcode', - '/registration/account-data', - '/registration/agency-selection' - ] - }, - component: NewRegistration - } - ]; - - return ( - - - - ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss deleted file mode 100644 index c87469db5..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss +++ /dev/null @@ -1,63 +0,0 @@ -.consultingTypes { - padding: 0; - - @include breakpoint($fromMedium) { - width: 490px; - } - - &__intro { - font-size: $font-size-h3; - line-height: 32px; - } - - &__groupSelect { - max-width: 100%; - margin: $grid-base-three 0; - padding: 0; - } - - &__children { - margin: $grid-base-six -#{$grid-base-three} 0; - } - - &__learnMore { - display: flex; - align-items: flex-start; - margin-top: $grid-base-eight; - appearance: none; - border: none; - background: transparent; - cursor: pointer; - text-decoration: underline; - padding-left: 0; - - &:hover { - color: var(--skin-color-primary-hover, $hover-primary); - } - } - - &__learnMoreIcon { - margin-right: $grid-base; - position: relative; - color: $primary; - } - - &__learnMoreText { - color: $primary; - &:hover { - color: var(--skin-color-primary-hover, $hover-primary); - } - } - - &__explanationOverlay .overlay__content { - text-align: left; - } - - .button-as-link { - &:focus-visible { - outline: $focus-outline; - outline-offset: 4px; - border-radius: $login-button-focus-border-radius; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypes.tsx b/src/extensions/components/consultingTypes/ConsultingTypes.tsx deleted file mode 100644 index 2ec79a776..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypes.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import * as React from 'react'; -import { useEffect, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { apiGetConsultingTypeGroups } from '../../api/apiGetConsultingTypeGroups'; -import { ConsultingTypeGroupInterface } from '../../globalState/interfaces/ConsultingTypeGroupInterface'; -import { Stage } from '../stage/stage'; -import { ConsultingTypesGroupChild } from './ConsultingTypesGroupChild'; -import { ConsultingTypesOverlay } from './ConsultingTypesOverlay'; -import './ConsultingTypes.styles.scss'; -import useIsFirstVisit from '../../../utils/useIsFirstVisit'; -import { Headline } from '../../../components/headline/Headline'; -import { InfoIcon } from '../../../resources/img/icons'; -import { LoadingIndicator } from '../../../components/loadingIndicator/LoadingIndicator'; -import { SelectDropdown } from '../../../components/select/SelectDropdown'; -import { StageLayout } from '../../../components/stageLayout/StageLayout'; -import { Text } from '../../../components/text/Text'; - -export const ConsultingTypes = () => { - const { t: translate } = useTranslation(['common', 'consultingTypes']); - - const [loadedConsultingTypeGroups, setLoadedConsultingTypeGroups] = - useState([]); - - const [consultingTypeGroups, setConsultingTypeGroups] = useState< - (ConsultingTypeGroupInterface & { - id: string; - })[] - >([]); - - const [loading, setLoading] = useState(true); - const [selectedGroupId, setSelectedGroupId] = useState(null); - const [expandedConsultingTypeId, setExpandedConsultingTypeId] = - useState(null); - const [isExplanationOverlayOpen, setIsExplanationOverlayOpen] = - useState(false); - const [selectedGroup, setSelectedGroup] = useState(null); - - useEffect(() => { - if (selectedGroupId === null) { - return; - } - setSelectedGroup( - consultingTypeGroups.find((group) => group.id === selectedGroupId) - ); - }, [selectedGroupId, consultingTypeGroups]); - - const handleGroupSelect = useCallback((item) => { - setSelectedGroupId(item.value); - setExpandedConsultingTypeId(undefined); - }, []); - - const handleConsultingTypeToggle = useCallback( - (consultingTypeId) => { - setExpandedConsultingTypeId( - consultingTypeId === expandedConsultingTypeId - ? undefined - : consultingTypeId - ); - }, - [expandedConsultingTypeId] - ); - - const handleExplanationOverlayToggle = () => { - setIsExplanationOverlayOpen(!isExplanationOverlayOpen); - }; - - const sortConsultingTypeGroups = useCallback( - (groups: ConsultingTypeGroupInterface[]) => - groups - // Translate titles before sorting - .map( - ( - group - ): ConsultingTypeGroupInterface & { - id: string; - } => ({ - ...group, - id: group.consultingTypes - .map((consultingType) => consultingType.id) - .sort() - .join('-'), - title: translate( - [ - `consultingTypeGroup.${group.title}.title`, - group.title - ], - { ns: 'consultingTypes' } - ) - }) - ) - .sort((a, b) => (a.title < b.title ? -1 : 1)) - .map((group) => ({ - ...group, - consultingTypes: group.consultingTypes - .slice() - // Translate titles before sorting - .map((consultingType) => ({ - ...consultingType, - titles: { - ...consultingType.titles, - long: translate( - [ - `consultingType.${consultingType.id}.titles.long`, - consultingType.titles.long - ], - { ns: 'consultingTypes' } - ), - default: translate( - [ - `consultingType.${consultingType.id}.titles.default`, - consultingType.titles.default - ], - { ns: 'consultingTypes' } - ) - } - })) - .sort((a, b) => - a.titles.long < b.titles.long ? -1 : 1 - ) - })), - [translate] - ); - - useEffect(() => { - apiGetConsultingTypeGroups() - .then(setLoadedConsultingTypeGroups) - .catch((error) => { - console.log(error); - }); - }, []); - - useEffect(() => { - if (!loadedConsultingTypeGroups) { - return; - } - - setConsultingTypeGroups( - sortConsultingTypeGroups(loadedConsultingTypeGroups) - ); - setLoading(false); - - return () => { - setLoading(true); - }; - }, [loadedConsultingTypeGroups, sortConsultingTypeGroups]); - - const isFirstVisit = useIsFirstVisit(); - - if (loading) { - return null; - } - - return ( - - } - > - {consultingTypeGroups == null ? ( - - ) : ( -
- - - ({ - value: group.id, - label: group.title - }))} - selectInputLabel={translate( - 'consultingTypes.selectGroup' - )} - handleDropdownSelect={handleGroupSelect} - useIconOption={false} - isSearchable={false} - menuPlacement="bottom" - /> -
- {selectedGroup !== null && - selectedGroup.consultingTypes.map( - (consultingType) => ( - - handleConsultingTypeToggle( - consultingType.id - ) - } - groupChild={consultingType} - /> - ) - )} -
- -
- )} - - {isExplanationOverlayOpen && ( - - )} -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss deleted file mode 100644 index c3d88ca86..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -.consultingTypesAgencySelection { - margin-top: $grid-base-three; - - &__innerWrapper { - @include breakpoint($xlarge) { - display: flex; - } - } - - &__postcode { - @include breakpoint($xlarge) { - max-width: 200px; - } - } - - &__register { - margin-top: $grid-base-three; - - button { - width: 100%; - } - - @include breakpoint($xlarge) { - margin-top: 0; - margin-left: $grid-base-two; - } - } - - &__postcodeFallback { - margin-top: $grid-base-three; - - p { - margin-bottom: 20px; - } - - a { - display: block; - text-decoration: underline; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx b/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx deleted file mode 100644 index 104fb1adb..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import './ConsultingTypesAgencySelection.styles.scss'; -import { useAppConfig } from '../../../hooks/useAppConfig'; -import { VALID_POSTCODE_LENGTH } from '../../../components/agencySelection/agencySelectionHelpers'; -import { apiAgencySelection, FETCH_ERRORS } from '../../../api'; -import { parsePlaceholderString } from '../../../utils/parsePlaceholderString'; -import { InputField } from '../../../components/inputField/InputField'; -import { PinIcon } from '../../../resources/img/icons'; -import { Button, BUTTON_TYPES } from '../../../components/button/Button'; -import { Notice } from '../../../components/notice/Notice'; -import { - AgencyDataInterface, - ConsultingTypeInterface -} from '../../../globalState/interfaces'; -import { AskerRegistrationExternalAgencyOverlay } from '../../../components/profile/AskerRegistrationExternalAgencyOverlay'; -import { Text } from '../../../components/text/Text'; - -interface ConsultingTypesAgencySelectionProps { - consultingType: ConsultingTypeInterface; -} - -const ConsultingTypesAgencySelection = ({ - consultingType -}: ConsultingTypesAgencySelectionProps) => { - const settings = useAppConfig(); - const [postcode, setPostcode] = useState(''); - const [agency, setAgency] = useState(); - const [postcodeFallbackLink, setPostcodeFallbackLink] = useState(); - const [externalAgencyOverlayActive, setExternalAgencyOverlayActive] = - useState(null); - const { t: translate } = useTranslation(); - - const handlePostcodeInput = (e) => { - setPostcode(e.target.value); - setPostcodeFallbackLink(null); - }; - - const handleButton = () => { - if (agency.external) { - if (!agency.url) { - console.error( - "Agency is external but didn't provide a `url`. Failed to redirect." - ); - return; - } - setExternalAgencyOverlayActive(true); - } else { - window.location.href = `/${consultingType.slug}/registration?postcode=${postcode}`; - } - }; - - useEffect(() => { - setAgency(undefined); - - if (postcode.length !== VALID_POSTCODE_LENGTH) { - return; - } - - let isCanceled = false; - apiAgencySelection({ - postcode, - consultingType: consultingType.id - }) - .then((result) => { - if (isCanceled) return; - - if (!result || result.length === 0) { - console.error( - 'Agency selection returned an empty result. This should never happen.' - ); - return; - } - setAgency(result[0]); - }) - .catch((error) => { - if (error.message === FETCH_ERRORS.EMPTY) { - setPostcodeFallbackLink( - parsePlaceholderString(settings.postcodeFallbackUrl, { - url: consultingType.urls - .registrationPostcodeFallbackUrl, - postcode: postcode - }) - ); - } - - return null; - }); - - return () => { - isCanceled = true; - }; - }, [ - consultingType.id, - consultingType.urls.registrationPostcodeFallbackUrl, - postcode, - settings.postcodeFallbackUrl - ]); - - const handleExternalAgencyOverlayAction = () => { - setExternalAgencyOverlayActive(false); - }; - - return ( -
-
- - }} - inputHandle={handlePostcodeInput} - /> -
- {postcodeFallbackLink != null && ( - - - - {translate( - 'registration.agencySelection.postcode.search' - )} - - - )} - {externalAgencyOverlayActive && ( - - )} -
- ); -}; - -export default ConsultingTypesAgencySelection; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss deleted file mode 100644 index fa4ae97c6..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss +++ /dev/null @@ -1,55 +0,0 @@ -.consultingTypesGroupChild { - &:last-of-type:not(.consultingTypesGroupChild--expanded) { - border-bottom: 1px solid $grey-light; - } - - &:not(.consultingTypesGroupChild--expanded) { - border-top: 1px solid $grey-light; - } - - &:hover { - background-color: $background-lighter; - } - - &--expanded { - border: none; - background-color: $background-lighter; - - & + .consultingTypesGroupChild { - border-top: none; - } - - .consultingTypesGroupChild__titleLabel { - font-weight: $font-weight-bold; - color: $secondary; - } - } - - &__title { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: $grid-base-three; - background: transparent; - appearance: none; - border: none; - cursor: pointer; - } - - &__titleLabel { - color: $primary; - } - - &__icon { - margin-left: $grid-base-three; - - path { - fill: $primary-4; - } - } - - &__details { - padding: 0 $grid-base-three $grid-base-three; - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx b/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx deleted file mode 100644 index 69fd2a2c4..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from 'clsx'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { ConsultingTypesGroupChildDetails } from './ConsultingTypesGroupChildDetails'; -import { ConsultingTypeGroupChildInterface } from '../../globalState/interfaces/ConsultingTypeGroupInterface'; -import { ArrowDownIcon, ArrowUpIcon } from '../../../resources/img/icons'; -import { Text } from '../../../components/text/Text'; -import './ConsultingTypesGroupChild.styles.scss'; - -interface ConsultingTypesGroupChildProps { - groupChild: ConsultingTypeGroupChildInterface; - handleToggleExpanded(): void; - isExpanded: boolean; -} - -export const ConsultingTypesGroupChild = ({ - groupChild, - handleToggleExpanded, - isExpanded -}: ConsultingTypesGroupChildProps) => { - const { t: translate } = useTranslation(['consultingTypes']); - const iconProps = { - width: 14, - className: 'consultingTypesGroupChild__icon' - }; - - return ( -
- - {isExpanded && ( - - )} -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx b/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx deleted file mode 100644 index 960036f8b..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import ConsultingTypesAgencySelection from './ConsultingTypesAgencySelection'; -import './consultingTypesGroupChildDetails.styles.scss'; -import { useTranslation } from 'react-i18next'; -import { apiGetConsultingType } from '../../../api'; -import { LoadingIndicator } from '../../../components/loadingIndicator/LoadingIndicator'; -import { ConsultingTypeInterface } from '../../../globalState/interfaces'; -import { Text } from '../../../components/text/Text'; -import { ArrowRightIcon } from '../../../resources/img/icons'; -interface ConsultingTypesGroupChildDetailsProps { - className?: string; - consultingTypeId: number; -} - -export const ConsultingTypesGroupChildDetails = ({ - className, - consultingTypeId -}: ConsultingTypesGroupChildDetailsProps) => { - const [consultingType, setConsultingType] = - useState(); - const { t: translate } = useTranslation(['common', 'consultingTypes']); - - useEffect(() => { - let isCanceled; - - apiGetConsultingType({ consultingTypeId }).then((result) => { - if (isCanceled) return; - setConsultingType(result); - }); - return () => { - isCanceled = true; - }; - }, [consultingTypeId]); - - if (consultingType == null) { - return ( -
- -
- ); - } - - return ( -
- - {consultingType.furtherInformation.url && ( - - - - - )} - -
- {translate('consultingTypes.details.explanation.description') - .split('\n') - .map((part, index) => ( - - ))} -
- -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss deleted file mode 100644 index 9bf15f8e2..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss +++ /dev/null @@ -1,12 +0,0 @@ -.consultingTypesOverlay { - &__serviceExplanation { - display: grid; - grid-template-columns: auto; - padding: $grid-base-four 0 $grid-base; - row-gap: $grid-base-three; - - .text { - text-align: left; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx b/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx deleted file mode 100644 index 05448d865..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import './ConsultingTypesOverlay.styles.scss'; -import { - Overlay, - OVERLAY_FUNCTIONS -} from '../../../components/overlay/Overlay'; -import { ServiceExplanation } from '../../../components/serviceExplanation/ServiceExplanation'; -import { BUTTON_TYPES } from '../../../components/button/Button'; - -interface ConsultingTypesOverlayProps { - handleOverlay?(): void; -} - -export const ConsultingTypesOverlay = ({ - handleOverlay -}: ConsultingTypesOverlayProps) => { - const { t: translate } = useTranslation(); - return ( - - ), - buttonSet: [ - { - label: translate('consultingTypes.overlay.close'), - function: OVERLAY_FUNCTIONS.CLOSE, - type: BUTTON_TYPES.PRIMARY - } - ] - }} - handleOverlay={handleOverlay} - /> - ); -}; diff --git a/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss b/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss deleted file mode 100644 index d7abf504a..000000000 --- a/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss +++ /dev/null @@ -1,40 +0,0 @@ -.consultingTypesGroupChildDetails { - &__loadingIndicator { - display: flex; - justify-content: center; - padding: $grid-base-three; - - // Reserve some extra space to avoid the layout shifting too much - min-height: 280px; - } - - &__details { - display: inline-flex; - align-items: flex-start; - margin-top: $grid-base-two; - } - - &__detailsIcon { - position: relative; - margin-right: $grid-base * 0.5; - top: -3px; // Align to baseline - } - - &__detailsLabel { - color: $primary; - } - - &__explanationTitle { - margin-top: $grid-base-three; - font-weight: $font-weight-bold; - } - - &__explanationDescription { - margin-top: $grid-base * 1.5; - white-space: pre-wrap; - } - - &__explanationDescriptionPart { - margin-top: $grid-base * 0.5; - } -} diff --git a/src/extensions/components/registration/Registration.tsx b/src/extensions/components/registration/Registration.tsx index e75ee0d13..9b234d552 100644 --- a/src/extensions/components/registration/Registration.tsx +++ b/src/extensions/components/registration/Registration.tsx @@ -1,24 +1,30 @@ import { Typography, Link, Button, Box } from '@mui/material'; import * as React from 'react'; -import { useState, useEffect, useContext, useCallback } from 'react'; +import { useState, useEffect, useContext, useCallback, useMemo } from 'react'; +import { + Route, + Switch, + useHistory, + useLocation, + useParams, + useRouteMatch, + generatePath, + Link as RouterLink +} from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Helmet } from 'react-helmet'; + import { StageLayout } from '../../../components/stageLayout/StageLayout'; import useIsFirstVisit from '../../../utils/useIsFirstVisit'; import { ReactComponent as HelloBannerIcon } from '../../../resources/img/illustrations/hello-banner.svg'; import { StepBar } from './stepBar/StepBar'; -import { AccountData } from './accountData/AccountData'; -import { ZipcodeInput } from './zipcodeInput/ZipcodeInput'; -import { AgencySelection } from './agencySelection/AgencySelection'; -import { TopicSelection } from './topicSelection/TopicSelection'; -import { useHistory, useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { Link as RouterLink } from 'react-router-dom'; import { WelcomeScreen } from './welcomeScreen/WelcomeScreen'; import { RegistrationContext, TenantContext, - registrationSessionStorageKey + registrationSessionStorageKey, + RegistrationData } from '../../../globalState'; -import { Helmet } from 'react-helmet'; import { GlobalComponentContext } from '../../../globalState/provider/GlobalComponentContext'; import { OVERLAY_FUNCTIONS, @@ -32,29 +38,46 @@ import { endpoints } from '../../../resources/scripts/endpoints'; import { apiPostRegistration } from '../../../api'; import { useAppConfig } from '../../../hooks/useAppConfig'; import { REGISTRATION_DATA_VALIDATION } from './registrationDataValidation'; +import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; + +/** + * This type of registration is currently not supporting: + * - autoSelectPostcode because its loaded over the consultingType and + * + * MultiTenancy: + * Each consultingType in mongodb has stored the tenant id (One to One Relation) -> Tenant URL could load by consultingType by tenant alternative only one consultingType exits + * MultiTenancyWithSingleDomain: + * Each consultintType in mongodb has stored the tenant id but this relation could not be loaded because no idea which consultingType settings to load before agency is selected + * For Diakonie there is no consultingType tenant relation and every tenant could have different consultingType depending on agency. So before agency is selected no idea which consultingType settings to load before agency is selected + * @constructor + */ export const Registration = () => { const { t } = useTranslation(['common', 'consultingTypes', 'agencies']); const settings = useAppConfig(); const isFirstVisit = useIsFirstVisit(); + const location = useLocation(); + const history = useHistory(); + const { path } = useRouteMatch(); + const { step, topicSlug } = useParams<{ + step: string; + topicSlug: string; + }>(); + const { Stage } = useContext(GlobalComponentContext); const { disabledNextButton, - setDisabledNextButton, - updateSessionStorageWithPreppedData, - refreshSessionStorageRegistrationData, - sessionStorageRegistrationData, - availableSteps, - dataPrepForSessionStorage, - consultant, - isConsultantLink + updateRegistrationData, + registrationData, + availableSteps } = useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); const { tenant } = useContext(TenantContext); - const [currentStep, setCurrentStep] = useState(1); + + const [stepData, setStepData] = useState>({}); const [redirectOverlayActive, setRedirectOverlayActive] = useState(false); - const location = useLocation(); - const history = useHistory(); + const handleOverlayAction = (buttonFunction: string) => { if (buttonFunction === OVERLAY_FUNCTIONS.REDIRECT_WITH_BLUR) { redirectToApp(); @@ -74,27 +97,14 @@ export const Registration = () => { ] }; - const updateCurrentStep = () => { - const currentLocation = location?.pathname?.replace( - '/registration', - '' - ); - const step = availableSteps.findIndex( - (step) => step?.urlSuffix === currentLocation - ); - setCurrentStep(step === -1 ? 0 : step); - }; - - const checkForStepsWithMissingMandatoryFields = (): number[] => { - if (currentStep > 0) { + const checkForStepsWithMissingMandatoryFields = + useCallback((): number[] => { return availableSteps.reduce( (missingSteps, step, currentIndex) => { if ( step?.mandatoryFields?.some( (mandatoryField) => - sessionStorageRegistrationData?.[ - mandatoryField - ] === undefined + registrationData?.[mandatoryField] === undefined ) ) { return [...missingSteps, currentIndex]; @@ -103,62 +113,61 @@ export const Registration = () => { }, [] ); - } - return []; - }; + }, [availableSteps, registrationData]); - const onNextClick = () => { - updateSessionStorageWithPreppedData(); - }; + const onNextClick = useCallback(() => { + updateRegistrationData(stepData); + setStepData({}); + }, [updateRegistrationData, stepData]); - useEffect(() => { - setDisabledNextButton(true); - updateCurrentStep(); - refreshSessionStorageRegistrationData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location]); + const currStepIndex = useMemo( + () => availableSteps.findIndex(({ name }) => name === step), + [availableSteps, step] + ); useEffect(() => { // Check if mandatory fields from previous steps are missing const missingPreviousSteps = checkForStepsWithMissingMandatoryFields() .sort() - .filter((missingStep) => missingStep < currentStep); + .filter((missingStep) => missingStep < currStepIndex); if (missingPreviousSteps.length > 0) { history.push( - `/registration${ - availableSteps[missingPreviousSteps[0]]?.urlSuffix - }${location.search}` + `${generatePath(path, { topicSlug, step: availableSteps[missingPreviousSteps[0]]?.name })}${location.search}` ); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentStep, availableSteps, sessionStorageRegistrationData]); + }, [ + availableSteps, + checkForStepsWithMissingMandatoryFields, + history, + location.search, + currStepIndex, + path, + topicSlug + ]); const onRegisterClick = useCallback(() => { - const registrationData = { - ...sessionStorageRegistrationData, - ...dataPrepForSessionStorage, - agencyId: sessionStorageRegistrationData.agencyId.toString(), - postcode: sessionStorageRegistrationData.zipcode, + const data = { + ...registrationData, + ...stepData, + agencyId: registrationData.agency.id.toString(), + postcode: registrationData.zipcode, termsAccepted: 'true', preferredLanguage: 'de', - // ConsultingType and mainTopicId are identical for the MVP - consultingType: sessionStorageRegistrationData.mainTopicId, - ...(isConsultantLink - ? { consultantId: consultant?.consultantId } + consultingType: registrationData.agency.consultingType, + ...(preselectedConsultant + ? { consultantId: preselectedConsultant?.consultantId } : {}) }; if ( Object.keys(REGISTRATION_DATA_VALIDATION).every((item) => - REGISTRATION_DATA_VALIDATION[item].validation( - registrationData[item] - ) + REGISTRATION_DATA_VALIDATION[item].validation(data[item]) ) ) { apiPostRegistration( endpoints.registerAsker, - registrationData, + data, settings.multitenancyWithSingleDomainEnabled, tenant ).then(() => { @@ -167,14 +176,35 @@ export const Registration = () => { }); } }, [ - consultant?.consultantId, - dataPrepForSessionStorage, - isConsultantLink, - sessionStorageRegistrationData, + registrationData, + stepData, + preselectedConsultant, settings.multitenancyWithSingleDomainEnabled, tenant ]); + const [prevStepUrl, nextStepUrl] = useMemo( + () => [ + availableSteps[currStepIndex - 1] + ? `${generatePath(path, { topicSlug, step: availableSteps[currStepIndex - 1]?.name || 'welcome' })}${location.search}` + : null, + availableSteps[currStepIndex + 1] + ? `${generatePath(path, { topicSlug, step: availableSteps[currStepIndex + 1]?.name || 'welcome' })}${location.search}` + : null + ], + [availableSteps, currStepIndex, path, topicSlug, location.search] + ); + + const stepPaths = useMemo( + () => + availableSteps.reduce( + (acc, { name }) => + acc.concat(generatePath(path, { topicSlug, step: name })), + [] + ), + [availableSteps, path, topicSlug] + ); + return ( <> { width: '100%' }} > - {availableSteps[currentStep]?.component === 'welcome' ? ( - - ) : ( - <> + + -
+ name) + .join(',')} + > { {t('registration.headline')} - {} + - {availableSteps[currentStep]?.component === - 'topicSelection' && ( - - )} - {availableSteps[currentStep]?.component === - 'zipcode' && } - {availableSteps[currentStep]?.component === - 'agencySelection' && ( - - )} - {availableSteps[currentStep]?.component === - 'accountData' && } - - {availableSteps[currentStep]?.component !== - 'welcome' && ( + + {availableSteps.map( + ({ name, component: Component }) => ( + + + + ) + )} + + - - {currentStep > 0 && ( - - {t('registration.back')} - - )} - {currentStep === - availableSteps.length - 1 ? ( - - ) : ( - - )} - + {t('registration.back')} + + + {!nextStepUrl ? ( + + ) : ( + + )} - )} + - - )} +
+ + + +
{redirectOverlayActive && ( diff --git a/src/extensions/components/registration/accountData/AccountData.tsx b/src/extensions/components/registration/accountData/AccountData.tsx index cf76ecaef..6544aa7cc 100644 --- a/src/extensions/components/registration/accountData/AccountData.tsx +++ b/src/extensions/components/registration/accountData/AccountData.tsx @@ -7,7 +7,14 @@ import { FormControlLabel } from '@mui/material'; import * as React from 'react'; -import { useState, useContext, useEffect } from 'react'; +import { + useState, + useContext, + useEffect, + VFC, + Dispatch, + SetStateAction +} from 'react'; import { useTranslation } from 'react-i18next'; import PersonIcon from '@mui/icons-material/Person'; import LockIcon from '@mui/icons-material/Lock'; @@ -19,7 +26,7 @@ import { hasSpecialChar } from '../../../../utils/validateInputValue'; import { LegalLinksContext } from '../../../../globalState/provider/LegalLinksProvider'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { apiGetIsUsernameAvailable } from '../../../../api/apiGetIsUsernameAvailable'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; import LegalLinks from '../../../../components/legalLinks/LegalLinks'; @@ -43,7 +50,9 @@ export const passwordCriteria = [ } ]; -export const AccountData = () => { +export const AccountData: VFC<{ + onChange: Dispatch>>; +}> = ({ onChange }) => { const legalLinks = useContext(LegalLinksContext); const { t } = useTranslation(); const [password, setPassword] = useState(''); @@ -56,8 +65,7 @@ export const AccountData = () => { const [username, setUsername] = useState(''); const [isUsernameAvailable, setIsUsernameAvailable] = useState(true); - const { setDisabledNextButton, setDataForSessionStorage } = - useContext(RegistrationContext); + const { setDisabledNextButton } = useContext(RegistrationContext); useEffect(() => { if ( @@ -68,7 +76,7 @@ export const AccountData = () => { dataProtectionChecked ) { setDisabledNextButton(false); - setDataForSessionStorage({ username, password }); + onChange({ username, password }); } else { setDisabledNextButton(true); } diff --git a/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx b/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx index 32d6d9f15..de71fa48e 100644 --- a/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx +++ b/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx @@ -1,6 +1,6 @@ import { Typography } from '@mui/material'; import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { apiAgencyLanguages } from '../../../../api/apiAgencyLanguages'; import { LanguagesContext } from '../../../../globalState/provider/LanguagesProvider'; @@ -11,32 +11,37 @@ interface AgencyLanguagesProps { export const AgencyLanguages = ({ agencyId }: AgencyLanguagesProps) => { const { t } = useTranslation(); - const [languages, setLanguages] = useState(['de']); - const { fixed: fixedLanguages } = React.useContext(LanguagesContext); + const [languagesString, setLanguagesString] = useState(''); + const { fixed: fixedLanguages } = useContext(LanguagesContext); useEffect(() => { - if (agencyId !== undefined) { - apiAgencyLanguages(agencyId, false).then((res) => { - const allLanguages = [...fixedLanguages, ...res.languages]; - setLanguages( - allLanguages - .filter((element, index) => { - return allLanguages.indexOf(element) === index; - }) - .sort() + (async () => { + let languages = ['de']; + if (agencyId !== undefined) { + languages = await apiAgencyLanguages(agencyId, false).then( + (res) => (languages = [...fixedLanguages, ...res.languages]) ); - }); - } - }, [agencyId, fixedLanguages]); + } + + setLanguagesString( + languages + .filter( + (element, index) => languages.indexOf(element) === index + ) + .map((lang) => { + const language = t(`languages.${lang}`); + const languageCode = lang.toUpperCase(); + return `${language} (${languageCode})`; + }) + .sort((a, b) => a.localeCompare(b)) + .join(' | ') + ); + })(); + }, [agencyId, fixedLanguages, t]); return ( - {languages.map( - (lang, index) => - `${index !== 0 ? ' |' : ''} ${t( - `languages.${lang}` - )} (${lang.toUpperCase()})` - )} + {languagesString} ); }; diff --git a/src/extensions/components/registration/agencySelection/AgencySelection.tsx b/src/extensions/components/registration/agencySelection/AgencySelection.tsx index ca9e6b66f..48a4954c4 100644 --- a/src/extensions/components/registration/agencySelection/AgencySelection.tsx +++ b/src/extensions/components/registration/agencySelection/AgencySelection.tsx @@ -1,88 +1,56 @@ import * as React from 'react'; -import { useState, useEffect, VFC, useContext } from 'react'; +import { + useState, + useEffect, + VFC, + useContext, + Dispatch, + SetStateAction +} from 'react'; import { AgencySelectionResults } from './AgencySelectionResults'; -import { apiAgencySelection } from '../../../../api'; -import { RegistrationContext } from '../../../../globalState'; -import { AgencyDataInterface } from '../../../../globalState/interfaces'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; +import { useAgenciesForRegistration } from '../../../../containers/registration/hooks/useAgenciesForRegistration'; export const AgencySelection: VFC<{ + onChange: Dispatch>>; nextStepUrl: string; onNextClick(): void; -}> = ({ nextStepUrl, onNextClick }) => { - const { - sessionStorageRegistrationData, - isConsultantLink, - consultant, - setDataForSessionStorage - } = useContext(RegistrationContext); - +}> = ({ nextStepUrl, onNextClick, onChange }) => { const { t } = useTranslation(); + const { registrationData } = useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); + + const { isLoading, agencies } = useAgenciesForRegistration({ + topic: registrationData.mainTopic, + postcode: registrationData?.zipcode + }); + const [headlineZipcode, setHeadlineZipcode] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [results, setResults] = useState( - undefined - ); - useEffect(() => { - if (isConsultantLink) { - setResults( - consultant?.agencies?.filter((agency) => - agency?.topicIds?.includes( - sessionStorageRegistrationData?.mainTopicId - ) - ) - ); - } else if (sessionStorageRegistrationData?.zipcode?.length === 5) { - setHeadlineZipcode(sessionStorageRegistrationData.zipcode); - setResults(undefined); - (async () => { - setIsLoading(true); - try { - const agencyResponse = await apiAgencySelection({ - postcode: sessionStorageRegistrationData.zipcode, - // We will keep consultingTypeId identical to mainTopicId - consultingType: - sessionStorageRegistrationData.mainTopicId || - undefined, - topicId: - sessionStorageRegistrationData.mainTopicId || - undefined - }); - setResults(agencyResponse); - if ( - agencyResponse.every( - (agency) => - agency.id !== - sessionStorageRegistrationData.agencyId - ) - ) { - setDataForSessionStorage({ agencyId: undefined }); - } - } catch { - setResults([]); - } - setIsLoading(false); - })(); + useEffect(() => { + if (!preselectedConsultant && registrationData?.zipcode?.length === 5) { + setHeadlineZipcode(registrationData.zipcode); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionStorageRegistrationData, consultant]); + }, [registrationData, preselectedConsultant]); return ( <> - {(isConsultantLink && consultant?.agencies?.length === 1) || - results?.length === 1 + {preselectedConsultant?.agencies?.length === 1 || + agencies?.length === 1 ? t('registration.agency.consultantheadline') : t('registration.agency.headline')} ); diff --git a/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx b/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx index 2ae005206..c7a2e510d 100644 --- a/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx +++ b/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Typography, FormControlLabel, @@ -8,24 +9,30 @@ import { Button, Link } from '@mui/material'; -import * as React from 'react'; -import { useContext, useEffect, useState } from 'react'; +import { + Dispatch, + SetStateAction, + useContext, + useEffect, + useState +} from 'react'; import TaskAltIcon from '@mui/icons-material/TaskAlt'; import NoResultsIllustration from '../../../../resources/img/illustrations/no-results.svg'; import ConsultantIllustration from '../../../../resources/img/illustrations/consultant-found.svg'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { Loading } from '../../../../components/app/Loading'; import { useTranslation } from 'react-i18next'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { AgencyDataInterface } from '../../../../globalState/interfaces'; import { AgencyLanguages } from './AgencyLanguages'; import { parsePlaceholderString } from '../../../../utils/parsePlaceholderString'; import { useAppConfig } from '../../../../hooks/useAppConfig'; -import { VFC } from 'react'; import { MetaInfo } from '../metaInfo/MetaInfo'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; interface AgencySelectionResultsProps { + onChange: Dispatch>>; isLoading?: boolean; zipcode?: string; results?: AgencyDataInterface[]; @@ -33,24 +40,22 @@ interface AgencySelectionResultsProps { onNextClick(): void; } -export const AgencySelectionResults: VFC = ({ +export const AgencySelectionResults = ({ + onChange, isLoading, zipcode, results, nextStepUrl, onNextClick -}) => { +}: AgencySelectionResultsProps) => { const { t } = useTranslation(); const settings = useAppConfig(); - const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData, - isConsultantLink - } = useContext(RegistrationContext); + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); - const [agencyId, setAgencyId] = useState( - sessionStorageRegistrationData?.agencyId + const [selectedAgency, setSelectedAgency] = useState( + registrationData?.agency ); useEffect(() => { @@ -59,10 +64,10 @@ export const AgencySelectionResults: VFC = ({ results?.length > 0 && results?.every((agency) => agency.external) ) { - setAgencyId(undefined); + setSelectedAgency(undefined); setDisabledNextButton(true); - setDataForSessionStorage({ - agencyId: undefined + onChange({ + agency: undefined }); return; } @@ -71,43 +76,38 @@ export const AgencySelectionResults: VFC = ({ results?.length === 1 && results?.every((agency) => !agency.external) ) { - setAgencyId(results[0].id); + setSelectedAgency(results[0]); setDisabledNextButton(false); - setDataForSessionStorage({ - agencyId: results[0].id + onChange({ + agency: results[0] }); return; } + if ( // invalid agencyId, needs to be removed - agencyId && + selectedAgency && results?.length === 0 ) { setDisabledNextButton(true); - setDataForSessionStorage({ - agencyId: undefined + onChange({ + agency: undefined }); return; } + if ( // valid agencyId REGISTRATION_DATA_VALIDATION.agencyId.validation( - agencyId?.toString() + selectedAgency?.id?.toString() ) ) { setDisabledNextButton(false); - setDataForSessionStorage({ - agencyId: agencyId - }); - return; + onChange({ agency: selectedAgency }); } - }, [ - agencyId, - results, - setDataForSessionStorage, - setDisabledNextButton, - zipcode - ]); + }, [selectedAgency, results, onChange, setDisabledNextButton, zipcode]); + + const onlyExternalAgencies = results?.every((agency) => agency.external); return ( <> @@ -123,67 +123,62 @@ export const AgencySelectionResults: VFC = ({ )} - {!!results && !isLoading && !isConsultantLink && ( + {!!results && !isLoading && !preselectedConsultant && ( {t('registration.agency.result.headline') + ' ' + zipcode}: )} + {/* only external results */} - {results?.length > 0 && - results?.every((agency) => agency.external) && ( + {results?.length > 0 && onlyExternalAgencies && ( + + + + {t('registration.agency.result.external.headline')} + + + {t('registration.agency.result.external.subline')} + + {results?.[0]?.url && ( + + )} + - - - {t( - 'registration.agency.result.external.headline' - )} - - - {t( - 'registration.agency.result.external.subline' - )} - - {results?.[0]?.url && ( - - )} - - - - )} + /> + + )} + {/* no Results */} {results?.length === 0 && ( = ({ /> )} + {/* one Result */} - {results?.length === 1 && - !results?.every((agency) => { - return agency.external; - }) && ( - - + + - - - } - icon={} + + } + icon={} + /> + } + label={ + + + {results?.[0].name || ''} + + + {t( + 'registration.agency.result.languages' + )} + + + + } + /> + {results?.[0].description && ( + + setSelectedAgency(undefined) } - label={ - - - {results?.[0].name || ''} - - - {t( - 'registration.agency.result.languages' - )} - - - - } + backButtonLabel={t( + 'registration.agency.infoOverlay.backButtonLabel' + )} + nextButtonLabel={t( + 'registration.agency.infoOverlay.nextButtonLabel' + )} + nextStepUrl={nextStepUrl} + onNextClick={onNextClick} + onOverlayOpen={() => { + onChange({ + agency: results?.[0] + }); + setSelectedAgency(results?.[0]); + }} /> - {results?.[0].description && ( - - setAgencyId(undefined) - } - backButtonLabel={t( - 'registration.agency.infoOverlay.backButtonLabel' - )} - nextButtonLabel={t( - 'registration.agency.infoOverlay.nextButtonLabel' - )} - nextStepUrl={nextStepUrl} - onNextClick={onNextClick} - onOverlayOpen={() => { - setDataForSessionStorage({ - agencyId: results?.[0].id - }); - setAgencyId(results?.[0].id); - }} - /> - )} - - - - )} + )} + + + + )} + {/* more Results */} - {results?.length > 1 && - !results?.every((agency) => agency.external) && ( - - - {results?.map((agency, index) => ( + {results?.length > 1 && !onlyExternalAgencies && ( + + + {results + ?.filter((agency) => !agency.external) + .map((agency, index) => ( = ({ }} > { setDisabledNextButton(false); - setAgencyId(agency.id); - setDataForSessionStorage({ - agencyId: agency.id - }); + setSelectedAgency(agency); + onChange({ agency }); }} sx={{ alignItems: 'flex-start' @@ -354,7 +352,10 @@ export const AgencySelectionResults: VFC = ({ value={agency.id} control={ } label={ @@ -390,7 +391,7 @@ export const AgencySelectionResults: VFC = ({ headline={agency.name} description={agency.description} onOverlayClose={() => - setAgencyId(undefined) + setSelectedAgency(undefined) } backButtonLabel={t( 'registration.agency.infoOverlay.backButtonLabel' @@ -401,18 +402,16 @@ export const AgencySelectionResults: VFC = ({ nextStepUrl={nextStepUrl} onNextClick={onNextClick} onOverlayOpen={() => { - setDataForSessionStorage({ - agencyId: agency.id - }); - setAgencyId(agency.id); + onChange({ agency }); + setSelectedAgency(agency); }} /> )} ))} - - - )} + + + )} ); }; diff --git a/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx b/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx index d1c65e786..aa338c6f0 100644 --- a/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx +++ b/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; import { AgencySelection } from './AgencySelection'; import { endpoints } from '../../../../resources/scripts/endpoints'; import { RegistrationContext } from '../../../../globalState'; -import { BrowserRouter as Router } from 'react-router-dom'; it('Get results for zipcode', () => { cy.fixture('service.agencies.json').then((data) => { @@ -15,10 +15,10 @@ it('Get results for zipcode', () => { {}, - setDataForSessionStorage: () => {}, - isConsultantLink: false, - consultant: null, - sessionStorageRegistrationData: { + updateRegistrationData: () => {}, + registrationData: { + agency: null, + mainTopic: null, zipcode: '12345', username: null, agencyId: null, @@ -27,7 +27,11 @@ it('Get results for zipcode', () => { } }} > - {}} /> + {}} + onChange={() => {}} + /> ); diff --git a/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx b/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx index c10a35e00..6e349e8fe 100644 --- a/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx +++ b/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx @@ -1,31 +1,30 @@ import * as React from 'react'; -import { useState, useContext, useEffect } from 'react'; +import { useState, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { Box, SwipeableDrawer, Typography } from '@mui/material'; import { RegistrationContext } from '../../../../globalState'; import { PreselectionError } from '../preselectionError/PreselectionError'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; interface InfoDrawerProps { trigger?: boolean; } export const InfoDrawer = ({ trigger }: InfoDrawerProps) => { - const { - preselectedAgency, - preselectedTopicName, - hasAgencyError, - hasConsultantError, - hasTopicError, - isConsultantLink, - preselectedData - } = useContext(RegistrationContext); const { t } = useTranslation(); + + const { hasAgencyError, hasConsultantError, hasTopicError } = + useContext(RegistrationContext); + const { + topic: preselectedTopic, + agency: preselectedAgency, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); + const drawerBleeding = 92; const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [topicName, setTopicName] = useState('-'); - const [agencyName, setAgencyName] = useState('-'); const toggleDrawer = (event: React.KeyboardEvent | React.MouseEvent) => { if ( @@ -39,157 +38,142 @@ export const InfoDrawer = ({ trigger }: InfoDrawerProps) => { setIsDrawerOpen(!isDrawerOpen); }; - useEffect(() => { - if (preselectedTopicName) { - setTopicName(preselectedTopicName); - } - if (preselectedAgency) { - setAgencyName(preselectedAgency.name); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [preselectedAgency, preselectedTopicName]); - - if ( - !(preselectedData.includes('tid') || preselectedData.includes('aid')) && - !isConsultantLink - ) { + if (!preselectedTopic && !preselectedAgency && !preselectedConsultant) { return null; } return ( - <> - .MuiPaper-root': { - top: -drawerBleeding, - overflow: 'visible' - }, - 'top': 0 + .MuiPaper-root': { + top: -drawerBleeding, + overflow: 'visible' + }, + 'top': 0 + }} + anchor="top" + onClose={() => setIsDrawerOpen(false)} + onOpen={() => setIsDrawerOpen(true)} + open={isDrawerOpen} + ModalProps={{ + keepMounted: true + }} + > + { + if (e.key === 'Enter') { + toggleDrawer(e); + } }} - anchor="top" - onClose={() => setIsDrawerOpen(false)} - onOpen={() => setIsDrawerOpen(true)} - open={isDrawerOpen} - ModalProps={{ - keepMounted: true + tabIndex={0} + sx={{ + 'px': '16px', + 'pt': '16px', + 'mt': trigger ? 0 : '48px', + 'position': 'relative', + 'borderBottomLeftRadius': 8, + 'borderBottomRightRadius': 8, + 'visibility': 'visible', + 'right': 0, + 'left': 0, + 'width': '100vw', + 'backgroundColor': 'primary.main', + 'color': 'white', + 'animationName': 'slideIn', + 'animationDuration': '0.8s', + 'animationDelay': '0.3s', + 'animationFillMode': 'forwards', + '@keyframes slideIn': { + '0%': { + top: 0 + }, + '100%': { + top: drawerBleeding + } + } }} > { - if (e.key === 'Enter') { - toggleDrawer(e); - } - }} - tabIndex={0} sx={{ - 'px': '16px', - 'pt': '16px', - 'mt': trigger ? 0 : '48px', - 'position': 'relative', - 'borderBottomLeftRadius': 8, - 'borderBottomRightRadius': 8, - 'visibility': 'visible', - 'right': 0, - 'left': 0, - 'width': '100vw', - 'backgroundColor': 'primary.main', - 'color': 'white', - 'animationName': 'slideIn', - 'animationDuration': '0.8s', - 'animationDelay': '0.3s', - 'animationFillMode': 'forwards', - '@keyframes slideIn': { - '0%': { - top: 0 - }, - '100%': { - top: drawerBleeding - } - } + opacity: isDrawerOpen ? 1 : 0, + overflow: 'scroll', + maxHeight: isDrawerOpen ? '75vh' : 0 }} > - - {hasConsultantError ? ( - - ) : ( - <> - - {t('registration.topic.summary')} - - {hasTopicError && topicName === '-' ? ( - - ) : ( + {hasConsultantError ? ( + + ) : ( + <> + + {t('registration.topic.summary')} + + {hasTopicError && !preselectedTopic ? ( + + ) : ( + preselectedTopic && ( - {topicName} + {preselectedTopic.name} - )} - - {t('registration.agency.summary')} - - {hasAgencyError && agencyName === '-' ? ( - - ) : ( + ) + )} + + {t('registration.agency.summary')} + + {hasAgencyError && !preselectedAgency ? ( + + ) : ( + preselectedAgency && ( - {agencyName} + {preselectedAgency.name} - )} - - )} - - - {isDrawerOpen ? ( - - ) : ( - - )} - + ) + )} + + )} + + + {isDrawerOpen ? ( + + ) : ( + + )} - - + + ); }; diff --git a/src/extensions/components/registration/preselectionBox/PreselectedAgency.tsx b/src/extensions/components/registration/preselectionBox/PreselectedAgency.tsx new file mode 100644 index 000000000..3c8b99b20 --- /dev/null +++ b/src/extensions/components/registration/preselectionBox/PreselectedAgency.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from 'react-i18next'; +import { Typography } from '@mui/material'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; +import * as React from 'react'; +import { + AgencyDataInterface, + TopicsDataInterface +} from '../../../../globalState/interfaces'; + +const PreselectedAgency = ({ + hasError, + agency +}: { + hasError: boolean; + agency: AgencyDataInterface; +}) => { + const { t } = useTranslation(); + + if (!hasError && !agency) { + return null; + } + + return ( + <> + + {t('registration.agency.summary')} + + {hasError ? ( + + <> + + ) : ( + {agency.name} + )} + + ); +}; + +export default PreselectedAgency; diff --git a/src/extensions/components/registration/preselectionBox/PreselectedConsultant.tsx b/src/extensions/components/registration/preselectionBox/PreselectedConsultant.tsx new file mode 100644 index 000000000..efe4380ad --- /dev/null +++ b/src/extensions/components/registration/preselectionBox/PreselectedConsultant.tsx @@ -0,0 +1,29 @@ +import { useTranslation } from 'react-i18next'; +import { Typography } from '@mui/material'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; +import * as React from 'react'; + +const PreselectedConsultant = ({ hasError }: { hasError: boolean }) => { + const { t } = useTranslation(); + if (hasError) { + return ( + + + ); + } + + return {t('registration.consultantlink')}; +}; + +export default PreselectedConsultant; diff --git a/src/extensions/components/registration/preselectionBox/PreselectedTopic.tsx b/src/extensions/components/registration/preselectionBox/PreselectedTopic.tsx new file mode 100644 index 000000000..a28d75b80 --- /dev/null +++ b/src/extensions/components/registration/preselectionBox/PreselectedTopic.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next'; +import { SxProps, Theme, Typography } from '@mui/material'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; +import * as React from 'react'; +import { TopicsDataInterface } from '../../../../globalState/interfaces'; + +const PreselectedTopic = ({ + hasError, + topic, + sx +}: { + hasError: boolean; + topic: TopicsDataInterface; + sx: SxProps; +}) => { + const { t } = useTranslation(); + + if (!hasError && !topic) { + return null; + } + + return ( + <> + + {t('registration.topic.summary')} + + {hasError ? ( + + <> + + ) : ( + {topic.name} + )} + + ); +}; + +export default PreselectedTopic; diff --git a/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx b/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx index c74a7b7fd..37ce67274 100644 --- a/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx +++ b/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx @@ -1,47 +1,33 @@ import * as React from 'react'; -import { useContext, useState, useEffect, VFC } from 'react'; +import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Typography } from '@mui/material'; import { RegistrationContext } from '../../../../globalState'; import { PreselectionDrawer } from '../preselectionDrawer/preselectionDrawer'; import ReportProblemIcon from '@mui/icons-material/ReportProblem'; import { useResponsive } from '../../../../hooks/useResponsive'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; +import PreselectedConsultant from './PreselectedConsultant'; +import PreselectedTopic from './PreselectedTopic'; +import PreselectedAgency from './PreselectedAgency'; -export const PreselectionBox: VFC<{ +export const PreselectionBox = ({ + hasDrawer = false +}: { hasDrawer?: boolean; -}> = ({ hasDrawer = false }) => { - const { - preselectedAgency, - preselectedTopicName, - isConsultantLink, - hasConsultantError, - hasTopicError, - hasAgencyError, - preselectedData - } = useContext(RegistrationContext); +}) => { const { t } = useTranslation(); - const [topicName, setTopicName] = useState('-'); - const [agencyName, setAgencyName] = useState('-'); const { fromM } = useResponsive(); - useEffect(() => { - if (preselectedTopicName) { - setTopicName(preselectedTopicName); - } else { - setTopicName('-'); - } - if (preselectedAgency) { - setAgencyName(preselectedAgency?.name); - } else { - setAgencyName('-'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [preselectedAgency, preselectedTopicName]); + const { hasConsultantError, hasTopicError, hasAgencyError } = + useContext(RegistrationContext); + const { + agency: preselectedAgency, + topic: preselectedTopic, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); - if ( - !(preselectedData.includes('tid') || preselectedData.includes('aid')) && - (!isConsultantLink || !hasDrawer) - ) { + if (!preselectedTopic && !preselectedAgency && !preselectedConsultant) { return null; } @@ -58,104 +44,38 @@ export const PreselectionBox: VFC<{ borderRadius: '4px', border: '1px solid #c6c5c4' }} + data-cy="preselected" + data-cy-preselected-consultant={ + preselectedConsultant?.consultantId + } + data-cy-preselected-consultant-error={hasConsultantError} + data-cy-preselected-agency={preselectedAgency?.id} + data-cy-preselected-agency-error={hasAgencyError} + data-cy-preselected-topic={preselectedTopic?.id} + data-cy-preselected-topic-error={hasTopicError} > - {isConsultantLink && - (hasConsultantError ? ( - - - ) : ( - - {' '} - {t('registration.consultantlink')} - - ))} - {(preselectedTopicName || hasTopicError) && - !isConsultantLink && ( - <> - - {t('registration.topic.summary')} - - {hasTopicError && !preselectedTopicName ? ( - - <> - - ) : ( - - {preselectedTopicName} - - )} - - )} - {(preselectedAgency || hasAgencyError) && !isConsultantLink && ( + {preselectedConsultant ? ( + + ) : ( <> - - {t('registration.agency.summary')} - - {hasAgencyError && !preselectedAgency ? ( - - <> - - ) : ( - {preselectedAgency?.name} - )} + /> + )} - {hasDrawer && !fromM && ( - - )} + {hasDrawer && !fromM && } ); }; diff --git a/src/extensions/components/registration/preselectionDrawer/preselectionDrawer.tsx b/src/extensions/components/registration/preselectionDrawer/preselectionDrawer.tsx index a6746ada9..dc88c4b6a 100644 --- a/src/extensions/components/registration/preselectionDrawer/preselectionDrawer.tsx +++ b/src/extensions/components/registration/preselectionDrawer/preselectionDrawer.tsx @@ -1,29 +1,51 @@ import * as React from 'react'; -import { useState, VFC, useContext } from 'react'; +import { useState, useContext } from 'react'; import { Box, Drawer, Typography, Button } from '@mui/material'; import { ReactComponent as Loader } from './loader.svg'; import { ReactComponent as Logo } from './logo.svg'; import { useTranslation } from 'react-i18next'; import { PreselectionError } from '../preselectionError/PreselectionError'; import { RegistrationContext } from '../../../../globalState'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; -export const PreselectionDrawer: VFC<{ - topicName: string; - agencyName: string; -}> = ({ topicName, agencyName }) => { - const { - hasTopicError, - hasAgencyError, - hasConsultantError, - isConsultantLink - } = useContext(RegistrationContext); +const ConsultantPreslection = ({ hasError }) => { + const { t } = useTranslation(); + + if (hasError) { + return ( + + ); + } + + return ( + + {t('registration.consultantlink')} + + ); +}; + +export const PreselectionDrawer = () => { const { t } = useTranslation(); - const [loading, isLoading] = useState(true); + + const { hasTopicError, hasAgencyError, hasConsultantError } = + useContext(RegistrationContext); + const { + agency: preselectedAgency, + topic: preselectedTopic, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); + + const [isloading, setIsloading] = useState(true); const [isOverlayDrawerOpen, setIsOverlayDrawerOpen] = useState(true); setTimeout(() => { - isLoading(false); + setIsloading(false); }, 3000); return ( @@ -63,7 +85,7 @@ export const PreselectionDrawer: VFC<{ p: '24px' }} > - {loading && ( + {isloading && ( {t('app.claim')}
- {!loading && ( + {!isloading && ( - {isConsultantLink ? ( - hasConsultantError ? ( - - ) : ( - - {t('registration.consultantlink')} - - ) + {preselectedConsultant ? ( + ) : ( <> {t('registration.topic.summary')} - {hasTopicError && topicName === '-' ? ( + {hasTopicError ? ( + /> ) : ( - {topicName} + {preselectedTopic.name} )} {t('registration.agency.summary')} - {hasAgencyError && agencyName === '-' ? ( + {hasAgencyError ? ( + /> ) : ( - {agencyName} + {preselectedAgency.name} )} diff --git a/src/extensions/components/registration/stepBar/StepBar.tsx b/src/extensions/components/registration/stepBar/StepBar.tsx index f8bcce859..d216b01fe 100644 --- a/src/extensions/components/registration/stepBar/StepBar.tsx +++ b/src/extensions/components/registration/stepBar/StepBar.tsx @@ -15,7 +15,12 @@ export const StepBar: VFC = ({ const { t } = useTranslation(); return ( <> - + >>; nextStepUrl: string; onNextClick(): void; -}> = ({ nextStepUrl, onNextClick }) => { +}> = ({ onChange, nextStepUrl, onNextClick }) => { + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData, - preselectedData, - preselectedAgency, - isConsultantLink, - consultant, - hasAgencyError - } = useContext(RegistrationContext); + topic: preselectedTopic, + agency: preselectedAgency, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); const { t } = useTranslation(); const [value, setValue] = useState( - sessionStorageRegistrationData.mainTopicId || undefined + registrationData.mainTopicId || undefined ); - const [topicGroups, setTopicGroups] = useState([]); - const [topics, setTopics] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [topicGroups, setTopicGroups] = useState(); + const [topics, setTopics] = useState(); const [listView, setListView] = useState(false); const [topicGroupId, setTopicGroupId] = useState( - sessionStorageRegistrationData.topicGroupId || undefined + registrationData.topicGroupId || undefined ); - const getTopic = (mainTopicId: number) => { - return topics?.find((topic) => topic?.id === mainTopicId); - }; + const getTopic = useCallback( + (mainTopicId: number) => + topics?.find((topic) => topic?.id === mainTopicId), + [topics] + ); useEffect(() => { if ( REGISTRATION_DATA_VALIDATION.mainTopicId.validation( value?.toString() ) && - (topicGroups.some((topicGroup) => + (topicGroups?.some((topicGroup) => topicGroup.topicIds.includes(value) ) || - (listView && topics.some((topic) => topic.id === value))) + (listView && topics?.some((topic) => topic.id === value))) ) { setDisabledNextButton(false); } }, [setDisabledNextButton, value, topicGroups, listView, topics]); useEffect(() => { - if ( - (preselectedData.includes('aid') && !hasAgencyError) || - (isConsultantLink && consultant) - ) { - setListView(true); - } else { - setListView(false); - } - }, [consultant, hasAgencyError, isConsultantLink, preselectedData]); + setListView(!!(preselectedAgency || preselectedConsultant)); + }, [preselectedConsultant, preselectedAgency]); useEffect(() => { - if (topics.length === 1) { + if (topics?.length === 1) { setValue(topics[0].id); - setDataForSessionStorage({ - mainTopicId: topics[0].id + onChange({ + mainTopic: topics[0] }); } - }, [setDataForSessionStorage, topics]); + }, [topics, onChange]); useEffect(() => { - const getFilteredTopics = (topics: TopicsDataInterface[]) => { - if (preselectedData.includes('aid') && !hasAgencyError) { - const topicIds = preselectedAgency?.topicIds; - return topics?.filter((topic) => topicIds.includes(topic.id)); - } - if (isConsultantLink && consultant) { - const topicIds = consultant?.agencies - .map((agency) => agency.topicIds) - .flat(); - return topics?.filter((topic) => topicIds.includes(topic.id)); - } - return topics; - }; + const filterConsultantTopics = (t) => + !preselectedConsultant || + preselectedConsultant.agencies.some((a) => + a.topicIds?.includes(t.id) + ); + + const filterAgencyTopics = (t) => + !preselectedAgency || preselectedAgency.topicIds?.includes(t.id); + + const getFilteredTopics = (topics: TopicsDataInterface[]) => + topics + // Filter topic by preselected topic + .filter( + (t) => !preselectedTopic || t.id === preselectedTopic?.id + ) + // Filter topics by consultant topics + .filter(filterConsultantTopics) + // Filter topics by preselected agency + .filter(filterAgencyTopics); (async () => { + setTopics(undefined); + setTopicGroups(undefined); + + const topicsResponse = await apiGetTopicsData(); + const topics = getFilteredTopics(topicsResponse); try { - setIsLoading(true); + const topicIds = topics.map((t) => t.id); const topicGroupsResponse = await apiGetTopicGroups(); - const topicsResponse = await apiGetTopicsData(); - - setTopics(getFilteredTopics(topicsResponse)); + //const filer setTopicGroups( topicGroupsResponse.data.items .filter((topicGroup) => topicGroup.topicIds.length > 0) + .filter((topicGroup) => + topicGroup.topicIds.some(topicIds.includes) + ) .sort((a, b) => { if (a.name === b.name) return 0; return a.name < b.name ? -1 : 1; }) ); - setIsLoading(false); } catch { - setTopics([]); setTopicGroups([]); + setListView(true); } + + setTopics(topics); })(); - }, [ - consultant, - hasAgencyError, - isConsultantLink, - preselectedAgency, - preselectedData - ]); + }, [preselectedConsultant, preselectedAgency, preselectedTopic]); return ( <> - {topics.length === 1 ? ( + {topics?.length === 1 ? ( {t('registration.topic.oneResult')} @@ -148,7 +156,7 @@ export const TopicSelection: VFC<{ )} - {isLoading ? ( + {topics === undefined || topicGroups === undefined ? ( - {topicGroups && topics && listView - ? topics + {listView + ? (topics || []) .sort((a, b) => { if (a.name === b.name) return 0; return a.name < b.name ? -1 : 1; }) .map((topic, index) => ( - + setValue(undefined) + } + onOverlayOpen={() => + setValue(topic.id) + } + onChange={() => { + setValue(topic.id); + onChange({ + mainTopic: topic + }); }} - > - { - setValue(topic.id); - setDataForSessionStorage( - { - mainTopicId: - topic?.id - } - ); - }} - checked={ - value === topic?.id - } - checkedIcon={ - topics.length === - 1 ? ( - - ) : undefined - } - icon={ - topics.length === - 1 ? ( - - ) : undefined - } - /> - } - label={ - - - {topic?.name} - - - } - /> - {topic?.description && ( - - setValue(undefined) - } - backButtonLabel={t( - 'registration.topic.infoOverlay.backButtonLabel' - )} - nextButtonLabel={t( - 'registration.topic.infoOverlay.nextButtonLabel' - )} - nextStepUrl={nextStepUrl} - onNextClick={onNextClick} - onOverlayOpen={() => { - setDataForSessionStorage( - { - mainTopicId: - topic.id - } - ); - setValue(topic.id); - }} - /> - )} - + /> )) - : topicGroups.map((topicGroup) => ( + : (topicGroups || []).map((topicGroup) => ( - + {topicGroup.topicIds .map((t) => getTopic(t)) + .filter(Boolean) .sort((a, b) => { if (a.name === b.name) return 0; @@ -324,109 +276,48 @@ export const TopicSelection: VFC<{ : 1; }) .map((topic, index) => ( - { + setValue(undefined); + setTopicGroupId( + undefined + ); }} - > - { - setValue( - topic.id - ); - setTopicGroupId( - topicGroup.id - ); - setDataForSessionStorage( - { - mainTopicId: - topic?.id, - topicGroupId: - topicGroup?.id - } - ); - }} - checked={ - value === - topic?.id && - topicGroup.id === - topicGroupId - } - /> - } - label={ - - - { - topic?.name - } - - - } - /> - {topic.description && ( - - setValue( - undefined - ) - } - backButtonLabel={t( - 'registration.topic.infoOverlay.backButtonLabel' - )} - nextButtonLabel={t( - 'registration.topic.infoOverlay.nextButtonLabel' - )} - nextStepUrl={ - nextStepUrl - } - onNextClick={ - onNextClick - } - onOverlayOpen={() => { - setDataForSessionStorage( - { - mainTopicId: - topic.id, - topicGroupId: - topicGroup.id - } - ); - setValue( - topic.id - ); - setTopicGroupId( - topicGroup.id - ); - }} - /> - )} - + onOverlayOpen={() => { + setValue(topic.id); + setTopicGroupId( + topicGroup.id + ); + }} + checked={ + value === + topic.id && + topicGroup.id === + topicGroupId + } + onChange={() => { + setValue(topic.id); + setTopicGroupId( + topicGroup.id + ); + onChange({ + mainTopic: + topic, + topicGroupId: + topicGroup?.id + }); + }} + /> ))} @@ -437,3 +328,80 @@ export const TopicSelection: VFC<{ ); }; + +const TopicSelect = ({ + topics, + topic, + index, + nextStepUrl, + onNextClick, + onChange, + onOverlayClose, + onOverlayOpen, + checked +}) => { + const { t } = useTranslation(); + + return ( + + + ) : undefined + } + icon={ + topics.length === 1 ? ( + + ) : undefined + } + /> + } + label={ + + {topic?.name} + + } + /> + {topic?.description && ( + onOverlayClose(topic)} + onOverlayOpen={() => onOverlayOpen(topic)} + /> + )} + + ); +}; diff --git a/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx b/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx index eb6eaded4..87de83801 100644 --- a/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx +++ b/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx @@ -16,7 +16,11 @@ it('Get accordion content', () => { cy.mount( - {}} /> + {}} + onChange={() => {}} + /> ); diff --git a/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx b/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx index 146382005..da74de364 100644 --- a/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx +++ b/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx @@ -5,17 +5,16 @@ import CreateIcon from '@mui/icons-material/Create'; import ChatIcon from '@mui/icons-material/Chat'; import MailIcon from '@mui/icons-material/Mail'; import LockIcon from '@mui/icons-material/Lock'; -import { Link as RouterLink, useLocation } from 'react-router-dom'; -import { useMemo, VFC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { useMemo } from 'react'; import { PreselectionBox } from '../preselectionBox/PreselectionBox'; interface WelcomeScreenProps { nextStepUrl: string; } -export const WelcomeScreen: VFC = ({ nextStepUrl }) => { +export const WelcomeScreen = ({ nextStepUrl }: WelcomeScreenProps) => { const { t } = useTranslation(); - const { search } = useLocation(); const infoDefinitions = useMemo( () => [ @@ -73,7 +72,10 @@ export const WelcomeScreen: VFC = ({ nextStepUrl }) => { {t('registration.welcomeScreen.subline')} {infoDefinitions.map((info) => ( - + {info.icon} {info.headline} @@ -112,7 +114,8 @@ export const WelcomeScreen: VFC = ({ nextStepUrl }) => { sx={{ mt: { xs: '8px', md: '16px' } }} variant="contained" component={RouterLink} - to={`${nextStepUrl}${search}`} + to={nextStepUrl} + data-cy="button-register" > {t('registration.welcomeScreen.register.buttonLabel')} diff --git a/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx b/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx index 5d547e10d..4f132b2a9 100644 --- a/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx +++ b/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx @@ -1,33 +1,35 @@ import { InputAdornment, Typography } from '@mui/material'; import * as React from 'react'; import FmdGoodIcon from '@mui/icons-material/FmdGood'; -import { useState, VFC, useContext, useEffect } from 'react'; +import { + useState, + VFC, + useContext, + useEffect, + Dispatch, + SetStateAction +} from 'react'; import { useTranslation } from 'react-i18next'; import { Input } from '../../../../components/input/input'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; -export const ZipcodeInput: VFC = () => { +export const ZipcodeInput: VFC<{ + onChange: Dispatch>>; +}> = ({ onChange }) => { const { t } = useTranslation(); - const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData - } = useContext(RegistrationContext); - const [value, setValue] = useState( - sessionStorageRegistrationData.zipcode || '' - ); + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); + const [value, setValue] = useState(registrationData.zipcode || ''); useEffect(() => { if (REGISTRATION_DATA_VALIDATION.zipcode.validation(value)) { setDisabledNextButton(false); - setDataForSessionStorage({ zipcode: value }); + onChange({ zipcode: value }); } else { setDisabledNextButton(true); } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); + }, [setDisabledNextButton, onChange, value]); return ( <> @@ -40,6 +42,9 @@ export const ZipcodeInput: VFC = () => { {t('registration.zipcode.bullet1')} {t('registration.zipcode.bullet2')} { cy.mount( - + {}} /> ); @@ -18,7 +18,7 @@ it('shows correct value', () => { cy.mount( - + {}} /> ); @@ -30,7 +30,7 @@ it('show invalid input onBlur', () => { cy.mount( - + {}} /> ); @@ -43,7 +43,7 @@ it('does not show wrong characters', () => { cy.mount( - + {}} /> ); diff --git a/src/extensions/components/stage/stage.styles.scss b/src/extensions/components/stage/stage.styles.scss index 3af6a0cfb..81d2ac3d5 100644 --- a/src/extensions/components/stage/stage.styles.scss +++ b/src/extensions/components/stage/stage.styles.scss @@ -83,7 +83,7 @@ $animation-easing-title: cubic-bezier(0, 0, 0.5, 1); } } - &--animation-finished { + &--ready { transition-delay: unset; } diff --git a/src/extensions/components/stage/stage.tsx b/src/extensions/components/stage/stage.tsx index 630813fd5..820cdecbf 100644 --- a/src/extensions/components/stage/stage.tsx +++ b/src/extensions/components/stage/stage.tsx @@ -52,7 +52,7 @@ export const Stage = ({ className={clsx(className, 'stage', { 'stage--no-animation': !hasAnimation, 'stage--open': isOpen || !hasAnimation, - 'stage--animation-finished': hasAnimationFinished + 'stage--ready': hasAnimationFinished })} data-cy="stage" > diff --git a/src/extensions/cypress/cypress.json b/src/extensions/cypress/cypress.json new file mode 100644 index 000000000..65181f130 --- /dev/null +++ b/src/extensions/cypress/cypress.json @@ -0,0 +1,14 @@ +{ + "e2e": { + "excludeSpecPattern": [ + "cypress/e2e/**/registration/base.cy.ts", + "cypress/e2e/**/registration/consultingType.cy.ts", + "cypress/e2e/**/registration/topic.cy.ts", + "cypress/e2e/**/login.cy.ts" + ], + "specPattern": ["src/extensions/cypress/e2e/**/*.cy.ts"] + }, + "component": { + "excludeSpecPattern": ["src/extensions/**/*.cy.ts"] + } +} diff --git a/src/extensions/cypress/e2e/login.cy.ts b/src/extensions/cypress/e2e/login.cy.ts new file mode 100644 index 000000000..1dbab6164 --- /dev/null +++ b/src/extensions/cypress/e2e/login.cy.ts @@ -0,0 +1,37 @@ +import { + closeWebSocketServer, + mockWebSocket, + startWebSocketServer +} from '../../../../cypress/support/websocket'; +import { config } from '../../resources/scripts/config'; + +describe('Login', () => { + before(() => { + startWebSocketServer(); + }); + + after(() => { + closeWebSocketServer(); + }); + + beforeEach(() => { + cy.willReturn('frontend.settings', config); + mockWebSocket(); + }); + + it('should be able to login', () => { + cy.login(); + + cy.get('#appRoot').should('exist'); + }); + + it('displays the consultingtype page at the root', () => { + cy.visit('/'); + cy.contains('Willkommen bei der Online-Beratung'); + }); + + it('displays the login for resorts', () => { + cy.visit('/suchtberatung'); + cy.contains('Login'); + }); +}); diff --git a/src/extensions/initApp.tsx b/src/extensions/initApp.tsx index 9a56184f1..54d3e427a 100644 --- a/src/extensions/initApp.tsx +++ b/src/extensions/initApp.tsx @@ -1,36 +1,72 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { ThemeProvider } from '@mui/material'; import { App } from '../components/app/app'; import { Stage } from './components/stage/stage'; -import { ConsultingTypes } from './components/consultingTypes/ConsultingTypes'; import { Imprint } from './components/legalInformationLinks/Imprint'; import { DataProtection } from './components/legalInformationLinks/DataProtection'; import { config, legalLinks } from './resources/scripts/config'; +import { UrlParamsProvider } from '../globalState/provider/UrlParamsProvider'; +import { RegistrationProvider } from '../globalState'; +import { lazy } from 'react'; +import '../resources/styles/mui-variables-mapping.scss'; +import theme from './theme'; +import { Redirect } from 'react-router-dom'; + +const Registration = lazy(() => + import('./components/registration/Registration').then((m) => ({ + default: m.Registration + })) +); + +const NewRegistration = () => ( + + + + + +); ReactDOM.render( - + ( + + ) }, - component: Imprint - }, - { - route: { - path: legalLinks.privacy + { + route: { + path: legalLinks.imprint + }, + component: Imprint }, - component: DataProtection - } - ]} - stageComponent={Stage} - />, + { + route: { + path: legalLinks.privacy + }, + component: DataProtection + } + ]} + stageComponent={Stage} + /> + , document.getElementById('appRoot') ); diff --git a/src/extensions/pages/app.html b/src/extensions/pages/app.html index 4ff1af7e3..7d37f67a4 100644 --- a/src/extensions/pages/app.html +++ b/src/extensions/pages/app.html @@ -1,4 +1,4 @@ - + @@ -28,7 +28,6 @@ -