From 949d61a5a6a16e97021482311cf98a8a2725eb8e Mon Sep 17 00:00:00 2001 From: Marc Itzenthaler Date: Tue, 20 Feb 2024 04:38:42 +0100 Subject: [PATCH] fix: registration consulting types --- .env.sample | 2 +- proxy/config.js | 12 +- src/api/apiAgencySelection.ts | 49 ++-- src/api/apiGetAgencyId.ts | 14 +- src/api/apiGetConsultant.ts | 52 +--- src/api/apiRegistrationNewConsultingTypes.ts | 6 +- .../ConsultingTypeAgencySelection.tsx | 116 +++++++-- .../formAccordion/FormAccordion.tsx | 4 +- src/components/login/Login.tsx | 45 +++- .../mainTopicSelection/MainTopicSelection.tsx | 36 ++- src/components/registration/Registration.tsx | 3 + .../registration/RegistrationForm.tsx | 15 +- .../serviceExplanation/ServiceExplanation.tsx | 6 +- .../ProposedAgencies/ProposedAgencies.tsx | 37 +-- .../hooks/useAgenciesForRegistration.ts | 69 ++++-- ...useConsultantAgenciesAndConsultingTypes.ts | 79 ------ .../hooks/useConsultantRegistrationData.ts | 116 +++++++++ .../AppConfig/AppConfigInterface.ts | 6 +- .../interfaces/UserDataInterface.ts | 6 +- .../provider/UrlParamsProvider.tsx | 26 +- src/utils/loadConsultingTypeForAgency.ts | 11 + src/utils/useUrlParamsLoader.tsx | 232 +++++++++++------- 22 files changed, 610 insertions(+), 332 deletions(-) create mode 100644 src/containers/registration/hooks/useConsultantRegistrationData.ts create mode 100644 src/utils/loadConsultingTypeForAgency.ts diff --git a/.env.sample b/.env.sample index 1c26ee184..8c4d581c1 100644 --- a/.env.sample +++ b/.env.sample @@ -25,7 +25,7 @@ REACT_APP_DISABLE_2FA_DUTY=0 ### Registration # Enable fallback loader for direct link registration where slug could not be matched (0/1) -FRONTEND_REGISTRATION_DIRECTLINK_FALLBACKLOADER_ENABLED= +FRONTEND_REGISTRATION_USE_CONSULTINGTYPE_SLUG= ### Weblate # Weblate host diff --git a/proxy/config.js b/proxy/config.js index 12e24da94..9b5d9e9b2 100644 --- a/proxy/config.js +++ b/proxy/config.js @@ -14,14 +14,8 @@ module.exports = { } }, registration: { - directlink: { - fallbackLoader: { - enabled: !!parseInt( - process.env - .FRONTEND_REGISTRATION_DIRECTLINK_FALLBACKLOADER_ENABLED || - '1' - ) - } - } + useConsultingTypeSlug: !!parseInt( + process.env.FRONTEND_REGISTRATION_USE_CONSULTINGTYPE_SLUG || '0' + ) } }; diff --git a/src/api/apiAgencySelection.ts b/src/api/apiAgencySelection.ts index 18eb3cfb4..21284ee86 100644 --- a/src/api/apiAgencySelection.ts +++ b/src/api/apiAgencySelection.ts @@ -2,12 +2,20 @@ import { endpoints } from '../resources/scripts/endpoints'; import { fetchData, FETCH_METHODS, FETCH_ERRORS } from './fetchData'; import { VALID_POSTCODE_LENGTH } from '../components/agencySelection/agencySelectionHelpers'; import { AgencyDataInterface } from '../globalState'; +import { loadConsultingTypeForAgency } from '../utils/loadConsultingTypeForAgency'; export const apiAgencySelection = async ( - params: { + { + fetchConsultingTypeDetails, + ...params + }: { postcode: string; consultingType: number | undefined; topicId?: number; + age?: number; + gender?: string; + counsellingRelation?: string; + fetchConsultingTypeDetails?: boolean; }, signal?: AbortSignal ): Promise | null> => { @@ -24,22 +32,35 @@ export const apiAgencySelection = async ( skipAuth: true, responseHandling: [FETCH_ERRORS.EMPTY], ...(signal && { signal: signal }) - }).then((result) => { - if (result) { - // External agencies should only be returned - // if there are no internal ones. - const internalAgencies = result.filter( - (agency) => !agency.external - ); - if (internalAgencies.length > 0) { - return internalAgencies; + }) + .then((result) => { + if (result) { + // External agencies should only be returned + // if there are no internal ones. + const internalAgencies = result.filter( + (agency) => !agency.external + ); + if (internalAgencies.length > 0) { + return internalAgencies; + } else { + return result; + } } else { return result; } - } else { - return result; - } - }); + }) + .then((agencies) => { + if (!fetchConsultingTypeDetails) { + return agencies; + } + + return Promise.all( + agencies.map( + async (agency) => + await loadConsultingTypeForAgency(agency) + ) + ); + }); } else { return null; } diff --git a/src/api/apiGetAgencyId.ts b/src/api/apiGetAgencyId.ts index 73d95bc68..08ed6ac13 100644 --- a/src/api/apiGetAgencyId.ts +++ b/src/api/apiGetAgencyId.ts @@ -1,9 +1,11 @@ import { endpoints } from '../resources/scripts/endpoints'; import { fetchData, FETCH_METHODS, FETCH_ERRORS } from './fetchData'; import { AgencyDataInterface } from '../globalState'; +import { loadConsultingTypeForAgency } from '../utils/loadConsultingTypeForAgency'; export const apiGetAgencyById = async ( - agencyId: any + agencyId: any, + fetchConsultingTypeDetails?: boolean ): Promise => { const url = endpoints.agencyServiceBase + '/' + agencyId; @@ -12,5 +14,13 @@ export const apiGetAgencyById = async ( method: FETCH_METHODS.GET, skipAuth: true, responseHandling: [FETCH_ERRORS.EMPTY, FETCH_ERRORS.NO_MATCH] - }).then((response) => response[0]); + }) + .then((response) => response[0]) + .then(async (agency) => { + if (!fetchConsultingTypeDetails) { + return agency; + } + + return await loadConsultingTypeForAgency(agency); + }); }; diff --git a/src/api/apiGetConsultant.ts b/src/api/apiGetConsultant.ts index 45430fb99..f66f91170 100644 --- a/src/api/apiGetConsultant.ts +++ b/src/api/apiGetConsultant.ts @@ -1,13 +1,11 @@ import { endpoints } from '../resources/scripts/endpoints'; import { fetchData, FETCH_METHODS, FETCH_ERRORS } from './fetchData'; import { ConsultantDataInterface } from '../globalState'; -import { apiGetConsultingType } from './apiGetConsultingType'; -import { apiGetConsultingTypes } from './apiGetConsultingTypes'; +import { loadConsultingTypeForAgency } from '../utils/loadConsultingTypeForAgency'; export const apiGetConsultant = async ( consultantId: any, - fetchConsultingTypes?: boolean, - consultingTypeDetail: 'full' | 'basic' = 'full' + fetchConsultingTypeDetails?: boolean ): Promise => { const url = endpoints.agencyConsultants + '/' + consultantId; @@ -17,43 +15,19 @@ export const apiGetConsultant = async ( skipAuth: true, responseHandling: [FETCH_ERRORS.EMPTY, FETCH_ERRORS.NO_MATCH] }).then((user) => { - if (!fetchConsultingTypes) { + if (!fetchConsultingTypeDetails) { return user; } - if (consultingTypeDetail === 'full') { - return Promise.all( - user.agencies.map(async (agency) => ({ - ...agency, - consultingTypeRel: await apiGetConsultingType({ - consultingTypeId: agency?.consultingType - }) - })) - ).then((agencies): ConsultantDataInterface => { - return { - ...user, - agencies - }; - }); - } - - if (consultingTypeDetail === 'basic') { - return apiGetConsultingTypes().then((consultingTypes) => { - const mappedUserAgencies = user.agencies.map((agency) => { - const consultingTypeRel = consultingTypes.filter( - (type) => type.id === agency.consultingType - )[0]; - return { - ...agency, - consultingTypeRel: { ...consultingTypeRel } - }; - }); - - return { - ...user, - agencies: mappedUserAgencies - }; - }); - } + return Promise.all( + user.agencies.map( + async (agency) => await loadConsultingTypeForAgency(agency) + ) + ).then( + (agencies): ConsultantDataInterface => ({ + ...user, + agencies + }) + ); }); }; diff --git a/src/api/apiRegistrationNewConsultingTypes.ts b/src/api/apiRegistrationNewConsultingTypes.ts index 28ee37922..9308dffc6 100644 --- a/src/api/apiRegistrationNewConsultingTypes.ts +++ b/src/api/apiRegistrationNewConsultingTypes.ts @@ -14,14 +14,16 @@ export const apiRegistrationNewConsultingTypes = async ( consultingType: number, agencyId: number, postcode: string, - consultantId?: string + consultantId?: string, + topicIds?: number[] ): Promise => { const url = endpoints.registerAskerNewConsultingType; const data = JSON.stringify({ postcode, agencyId, consultingType, - consultantId + consultantId, + ...(topicIds ? { topicIds: topicIds } : {}) }); return fetchData({ diff --git a/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx b/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx index dfb35f333..b1228eb3b 100644 --- a/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx +++ b/src/components/consultingTypeSelection/ConsultingTypeAgencySelection.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { AgencyDataInterface } from '../../globalState'; import './consultingTypeAgencySelection.styles'; import '../profile/profile.styles'; @@ -18,7 +18,10 @@ import { Text } from '../text/Text'; import { AgencyLanguages } from '../agencySelection/AgencyLanguages'; import { useTranslation } from 'react-i18next'; import { useAppConfig } from '../../hooks/useAppConfig'; -import { useConsultantAgenciesAndConsultingTypes } from '../../containers/registration/hooks/useConsultantAgenciesAndConsultingTypes'; +import { useConsultantRegistrationData } from '../../containers/registration/hooks/useConsultantRegistrationData'; +import { apiGetTopicsData } from '../../api/apiGetTopicsData'; +import { useTenant } from '../../globalState'; +import { UrlParamsContext } from '../../globalState/provider/UrlParamsProvider'; export interface ConsultingTypeAgencySelectionProps { onChange: Function; @@ -35,19 +38,69 @@ export const ConsultingTypeAgencySelection = ({ }: ConsultingTypeAgencySelectionProps) => { const { t: translate } = useTranslation(['common', 'consultingTypes']); const settings = useAppConfig(); + const tenantData = useTenant(); + const { + agency: preselectedAgency, + consultant, + topic: preselectedTopic + } = useContext(UrlParamsContext); + const [selectedConsultingTypeOption, setSelectedConsultingTypeOption] = useState(null); + const [selectedTopicOption, setSelectedTopicOption] = + useState(null); const [consultingTypeOptions, setConsultingTypeOptions] = useState< SelectOption[] >([]); const [agencyOptions, setAgencyOptions] = useState( [] ); + const [topicOptions, setTopicOptions] = useState([]); + + const topicsAreRequired = useMemo( + () => + tenantData?.settings?.topicsInRegistrationEnabled && + tenantData?.settings?.featureTopicsEnabled, + [ + tenantData?.settings?.topicsInRegistrationEnabled, + tenantData?.settings?.featureTopicsEnabled + ] + ); const { agencies: possibleAgencies, - consultingTypes: possibleConsultingTypes - } = useConsultantAgenciesAndConsultingTypes(); + consultingTypes: possibleConsultingTypes, + topicIds: possibleTopicIds + } = useConsultantRegistrationData(); + + useEffect(() => { + apiGetTopicsData() + // Filter topic by preselected topic + .then((topics) => + topics.filter( + (t) => !preselectedTopic || preselectedTopic.id === t.id + ) + ) + // Filter topics by consultant topics + .then((topics) => + topics.filter((t) => possibleTopicIds.includes(t.id)) + ) + // Filter topics by preselected agency + .then((topics) => + topics.filter( + (t) => + !preselectedAgency || + preselectedAgency.topicIds?.includes(t.id) + ) + ) + .then((topics) => + topics.map((t) => ({ + value: t.id.toString(), + label: t.name + })) + ) + .then(setTopicOptions); + }, [possibleTopicIds]); useEffect(() => { const consultingTypeOptions = possibleConsultingTypes.map( @@ -67,19 +120,21 @@ export const ConsultingTypeAgencySelection = ({ }, [possibleConsultingTypes, translate]); useEffect(() => { - if (!selectedConsultingTypeOption) { + if ( + !selectedConsultingTypeOption || + (topicsAreRequired && !selectedTopicOption) + ) { setAgencyOptions([]); onChange(null); return; } - const agencyOptions = settings.multitenancyWithSingleDomainEnabled - ? possibleAgencies - : possibleAgencies.filter( - (agency) => - agency.consultingType.toString() === - selectedConsultingTypeOption.value - ); + const agencyOptions = possibleAgencies.filter( + (agency) => + agency.consultingType.toString() === + selectedConsultingTypeOption.value && + agency.topicIds.includes(parseInt(selectedTopicOption.value)) + ); setAgencyOptions(agencyOptions); if (agencyOptions.length >= 1) { @@ -89,7 +144,8 @@ export const ConsultingTypeAgencySelection = ({ onChange, possibleAgencies, selectedConsultingTypeOption, - settings.multitenancyWithSingleDomainEnabled + topicsAreRequired, + selectedTopicOption ]); useEffect(() => { @@ -99,6 +155,15 @@ export const ConsultingTypeAgencySelection = ({ onValidityChange(agency ? VALIDITY_VALID : VALIDITY_INVALID); }, [agency]); // eslint-disable-line react-hooks/exhaustive-deps + const handleChange = useCallback((agency) => { + onChange({ + ...agency, + ...(topicsAreRequired + ? { topicIds: [parseInt(selectedTopicOption?.value)] } + : {}) + }); + }, []); + const consultingTypeSelect: SelectDropdownItem = { id: 'consultingTypeSelection', selectedOptions: consultingTypeOptions, @@ -110,12 +175,35 @@ export const ConsultingTypeAgencySelection = ({ defaultValue: selectedConsultingTypeOption }; + const topicSelect: SelectDropdownItem = { + id: 'topicSelection', + selectedOptions: topicOptions, + handleDropdownSelect: setSelectedTopicOption, + selectInputLabel: translate( + 'registration.consultingTypeAgencySelection.topic.select.label' + ), + menuPlacement: 'bottom', + defaultValue: selectedTopicOption + }; + if (possibleAgencies.length <= 1 && possibleConsultingTypes.length <= 1) { return null; } return (
+ {topicOptions.length > 1 && ( +
+ + +
+ )} + {consultingTypeOptions.length > 1 && (
diff --git a/src/components/formAccordion/FormAccordion.tsx b/src/components/formAccordion/FormAccordion.tsx index 150379ffd..0ab3c1b51 100644 --- a/src/components/formAccordion/FormAccordion.tsx +++ b/src/components/formAccordion/FormAccordion.tsx @@ -32,7 +32,7 @@ import { Button, BUTTON_TYPES, ButtonItem } from '../button/Button'; import { FormAccordionRegistrationText } from './FormAccordionRegistrationText'; import { setValueInCookie } from '../sessionCookie/accessSessionCookie'; import { ProposedAgencies } from '../../containers/registration/components/ProposedAgencies/ProposedAgencies'; -import { useConsultantAgenciesAndConsultingTypes } from '../../containers/registration/hooks/useConsultantAgenciesAndConsultingTypes'; +import { useConsultantRegistrationData } from '../../containers/registration/hooks/useConsultantRegistrationData'; import { FormAccordionData } from '../registration/RegistrationForm'; import { UrlParamsContext } from '../../globalState/provider/UrlParamsProvider'; import { TProvidedLegalLink } from '../../globalState/provider/LegalLinksProvider'; @@ -71,7 +71,7 @@ export const FormAccordion = ({ const { setSpecificAgency, specificAgency } = useContext( AgencySpecificContext ); - const { consultingTypes } = useConsultantAgenciesAndConsultingTypes(); + const { consultingTypes } = useConsultantRegistrationData(); const [activeItem, setActiveItem] = useState(1); diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx index 2bbb322f7..a5b842058 100644 --- a/src/components/login/Login.tsx +++ b/src/components/login/Login.tsx @@ -28,8 +28,10 @@ import { RocketChatGlobalSettingsContext, TenantContext, UserDataContext, + LocaleContext, + useTenant, UserDataInterface, - LocaleContext + AgencyDataInterface } from '../../globalState'; import '../../resources/styles/styles'; import './login.styles'; @@ -60,8 +62,9 @@ import { useSearchParam } from '../../hooks/useSearchParams'; import { getTenantSettings } from '../../utils/tenantSettingsHelper'; import { budibaseLogout } from '../budibase/budibaseLogout'; import { GlobalComponentContext } from '../../globalState/provider/GlobalComponentContext'; -import { useConsultantAgenciesAndConsultingTypes } from '../../containers/registration/hooks/useConsultantAgenciesAndConsultingTypes'; +import { useConsultantRegistrationData } from '../../containers/registration/hooks/useConsultantRegistrationData'; import { UrlParamsContext } from '../../globalState/provider/UrlParamsProvider'; +import { TopicsDataInterface } from '../../globalState/interfaces/TopicsDataInterface'; const regexAccountDeletedError = /account disabled/i; @@ -69,6 +72,7 @@ export const Login = () => { const settings = useAppConfig(); const { t: translate } = useTranslation(); const history = useHistory(); + const tenantData = useTenant(); const { locale, initLocale } = useContext(LocaleContext); const { tenant } = useContext(TenantContext); @@ -134,7 +138,8 @@ export const Login = () => { } }, [featureToolsEnabled, gcid]); - const [agency, setAgency] = useState(null); + const [agency, setAgency] = useState(null); + const [topic, setTopic] = useState(null); const [validity, setValidity] = useState(VALIDITY_INITIAL); const [registerOverlayActive, setRegisterOverlayActive] = useState(false); const [pwResetOverlayActive, setPwResetOverlayActive] = useState(false); @@ -191,8 +196,9 @@ export const Login = () => { const { agencies: possibleAgencies, - consultingTypes: possibleConsultingTypes - } = useConsultantAgenciesAndConsultingTypes(); + consultingTypes: possibleConsultingTypes, + topicIds: possibleTopicIds + } = useConsultantRegistrationData(); const registerOverlay = useMemo( (): OverlayItem => ({ @@ -229,7 +235,8 @@ export const Login = () => { agency.consultingTypeRel.id, agency.id, agency.postcode, - consultantId + consultantId, + agency.topicIds ) .catch((response) => response.json()) .then((response) => { @@ -288,15 +295,26 @@ export const Login = () => { [locale] ); + const topicsAreRequired = useMemo( + () => + tenantData?.settings?.topicsInRegistrationEnabled && + tenantData?.settings?.featureTopicsEnabled, + [ + tenantData?.settings?.topicsInRegistrationEnabled, + tenantData?.settings?.featureTopicsEnabled + ] + ); + useEffect(() => { if ( possibleAgencies.length === 1 && - possibleConsultingTypes.length === 1 + possibleConsultingTypes.length === 1 && + (!topicsAreRequired || possibleTopicIds.length === 1) ) { setAgency(possibleAgencies[0]); setValidity(VALIDITY_VALID); } - }, [possibleAgencies, possibleConsultingTypes]); + }, [possibleAgencies, possibleConsultingTypes, topicsAreRequired]); useEffect(() => { deleteCookieByName('tenantId'); @@ -334,9 +352,13 @@ export const Login = () => { if ( possibleAgencies.length === 1 && - possibleConsultingTypes.length === 1 + possibleConsultingTypes.length === 1 && + (!topicsAreRequired || possibleTopicIds.length === 1) ) { - handleRegistration(possibleAgencies[0]); + handleRegistration({ + ...possibleAgencies[0], + topicIds: possibleTopicIds + }); } else { setRegisterOverlayActive(true); } @@ -349,7 +371,8 @@ export const Login = () => { possibleConsultingTypes.length, reloadUserData, handleRegistration, - gcid + gcid, + topicsAreRequired ] ); diff --git a/src/components/mainTopicSelection/MainTopicSelection.tsx b/src/components/mainTopicSelection/MainTopicSelection.tsx index a66c112af..0f3395787 100644 --- a/src/components/mainTopicSelection/MainTopicSelection.tsx +++ b/src/components/mainTopicSelection/MainTopicSelection.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { useEffect, useState } from 'react'; import './MainTopicSelection.styles'; import { apiGetTopicsData } from '../../api/apiGetTopicsData'; @@ -9,7 +9,8 @@ import { VALIDITY_INVALID } from '../registration/registrationHelpers'; import { useTranslation } from 'react-i18next'; -import { TopicsDataInterface } from '../../globalState/interfaces/TopicsDataInterface'; +import { TopicsDataInterface } from '../../globalState/interfaces'; +import { UrlParamsContext } from '../../globalState/provider/UrlParamsProvider'; export interface MainTopicSelectionProps { name: string; @@ -26,13 +27,34 @@ export const MainTopicSelection = ({ }: MainTopicSelectionProps) => { const { t: translate } = useTranslation(); - const [topics, setTopics] = useState([]); + const { agency, consultant, topic } = useContext(UrlParamsContext); + + const [loadedTopics, setLoadedTopics] = useState([]); const [isTouched, setIsTouched] = useState(false); useEffect(() => { - apiGetTopicsData().then((data) => setTopics(data)); + apiGetTopicsData().then(setLoadedTopics); }, []); + /* Handle url parameter preselection logic */ + const topics = useMemo( + () => + loadedTopics + // Filter topic by preselected topic + .filter((t) => !topic || t.id === topic?.id) + // Filter topics by consultant topics + .filter( + (t) => + !consultant || + consultant.agencies.some((a) => + a.topicIds?.includes(t.id) + ) + ) + // Filter topics by preselected agency + .filter((t) => !agency || agency.topicIds?.includes(t.id)), + [loadedTopics, agency, consultant] + ); + useEffect(() => { if (!isTouched) return; @@ -47,6 +69,12 @@ export const MainTopicSelection = ({ [onChange] ); + useEffect(() => { + if (topics.length === 1) { + handleChange(topics[0]); + } + }, [topics, handleChange]); + // If options change, check for still valid preselected topic useEffect(() => { if (!value || topics.some((t) => t.id === value.id)) { diff --git a/src/components/registration/Registration.tsx b/src/components/registration/Registration.tsx index 35bf44644..dde51e45c 100644 --- a/src/components/registration/Registration.tsx +++ b/src/components/registration/Registration.tsx @@ -13,6 +13,7 @@ import useIsFirstVisit from '../../utils/useIsFirstVisit'; import { useTranslation } from 'react-i18next'; import { GlobalComponentContext } from '../../globalState/provider/GlobalComponentContext'; import { UrlParamsContext } from '../../globalState/provider/UrlParamsProvider'; +import { useAppConfig } from '../../hooks/useAppConfig'; interface RegistrationProps { handleUnmatchConsultingType: Function; @@ -34,6 +35,7 @@ export const Registration = ({ const agencyId = getUrlParameter('aid'); const consultantId = getUrlParameter('cid'); const postcodeParameter = getUrlParameter('postcode'); + const settings = useAppConfig(); const { setInformal } = useContext(InformalContext); const { Stage } = useContext(GlobalComponentContext); @@ -68,6 +70,7 @@ export const Registration = ({ console.error( 'No `consultingType`, `consultant` or `agency` found in URL.' ); + window.location.href = settings.urls.landingpage; return; } diff --git a/src/components/registration/RegistrationForm.tsx b/src/components/registration/RegistrationForm.tsx index 7c5afa28b..848ef4337 100644 --- a/src/components/registration/RegistrationForm.tsx +++ b/src/components/registration/RegistrationForm.tsx @@ -49,7 +49,7 @@ export const RegistrationForm = () => { const { locale } = useLocaleData(); const settings = useAppConfig(); const postcode = getUrlParameter('postcode'); - const { agency, consultingType, consultant, topic } = + const { agency, consultingType, consultant, topic, slugFallback } = useContext(UrlParamsContext); const [formAccordionData, setFormAccordionData] = @@ -118,6 +118,10 @@ export const RegistrationForm = () => { setIsUsernameAlreadyInUse(false); setIsSubmitButtonDisabled(true); + const { autoSelectPostcode } = + formAccordionData.consultingType?.registration || + ConsultingTypeRegistrationDefaults; + const registrationData = { username: formAccordionData.username, password: encodeURIComponent(formAccordionData.password), @@ -129,7 +133,14 @@ export const RegistrationForm = () => { preferredLanguage: locale, ...(formAccordionData.state && { state: formAccordionData.state }), ...(formAccordionData.age && { age: formAccordionData.age }), - ...(consultant && { consultantId: consultant.consultantId }) + ...(consultant && { consultantId: consultant.consultantId }), + ...(slugFallback && { + consultingType: + formAccordionData.agency.consultingTypeRel?.id?.toString(), + postcode: autoSelectPostcode + ? formAccordionData.agency.postcode + : formAccordionData.postcode + }) }; const missingFields = [ diff --git a/src/components/serviceExplanation/ServiceExplanation.tsx b/src/components/serviceExplanation/ServiceExplanation.tsx index b968bea91..f04e983f5 100644 --- a/src/components/serviceExplanation/ServiceExplanation.tsx +++ b/src/components/serviceExplanation/ServiceExplanation.tsx @@ -44,7 +44,8 @@ export const ServiceExplanation = ({ title: translate( [ `consultingType.${consultingTypeId}.welcomeScreen.anonymous.title`, - welcomeScreenConfig?.anonymous.title ?? + `consultingType.fallback.welcomeScreen.anonymous.title`, + welcomeScreenConfig?.anonymous.title || 'registration.welcomeScreen.info4.title' ], { ns: ['consultingTypes', 'common'] } @@ -52,7 +53,8 @@ export const ServiceExplanation = ({ text: translate( [ `consultingType.${consultingTypeId}.welcomeScreen.anonymous.text`, - welcomeScreenConfig?.anonymous.text ?? + `consultingType.fallback.welcomeScreen.anonymous.text`, + welcomeScreenConfig?.anonymous.text || 'registration.welcomeScreen.info4.text' ], { ns: ['consultingTypes', 'common'] } diff --git a/src/containers/registration/components/ProposedAgencies/ProposedAgencies.tsx b/src/containers/registration/components/ProposedAgencies/ProposedAgencies.tsx index 1e3863354..4d798849b 100644 --- a/src/containers/registration/components/ProposedAgencies/ProposedAgencies.tsx +++ b/src/containers/registration/components/ProposedAgencies/ProposedAgencies.tsx @@ -45,7 +45,8 @@ export const ProposedAgencies = ({ }: ProposedAgenciesProps) => { const { t } = useTranslation(); - const { agency: preSelectedAgency } = useContext(UrlParamsContext); + const { agency: preSelectedAgency, slugFallback } = + useContext(UrlParamsContext); const [isTouched, setIsTouched] = useState(false); @@ -79,19 +80,21 @@ export const ProposedAgencies = ({ const handleAgencyChange = useCallback( (agency: AgencyDataInterface, isTouched = true) => { handleChange( - { - agency, - consultingType: consultingTypes.find( - (ct) => ct.id === agency?.consultingType - ), - ...(autoSelectPostcode - ? { postcode: agency?.postcode } - : {}) - }, + slugFallback + ? { + agency + } + : { + agency, + consultingType: agency?.consultingTypeRel, + ...(autoSelectPostcode + ? { postcode: agency?.postcode } + : {}) + }, isTouched ); }, - [autoSelectPostcode, consultingTypes, handleChange] + [autoSelectPostcode, handleChange, slugFallback] ); // If options change, check for still valid preselected agency @@ -119,14 +122,13 @@ export const ProposedAgencies = ({ agencies, autoSelectAgency, formAccordionData.agency, - onChange, - handleAgencyChange, - preSelectedAgency + handleAgencyChange ]); // If options change, check for still valid consulting type or select first one useEffect(() => { if ( + slugFallback || (formAccordionData.consultingType && consultingTypes.some( (ct) => ct.id === formAccordionData.consultingType.id @@ -137,7 +139,12 @@ export const ProposedAgencies = ({ } return onChange({ consultingType: consultingTypes?.[0] || null }); - }, [consultingTypes, formAccordionData.consultingType, onChange]); + }, [ + consultingTypes, + formAccordionData.consultingType, + onChange, + slugFallback + ]); // Validate form if there are no changeable fields or changeable fields and they are touched useEffect(() => { diff --git a/src/containers/registration/hooks/useAgenciesForRegistration.ts b/src/containers/registration/hooks/useAgenciesForRegistration.ts index 322ef7e9b..7859a5768 100644 --- a/src/containers/registration/hooks/useAgenciesForRegistration.ts +++ b/src/containers/registration/hooks/useAgenciesForRegistration.ts @@ -7,7 +7,7 @@ import { ConsultingTypeInterface, useTenant } from '../../../globalState'; -import { useConsultantAgenciesAndConsultingTypes } from './useConsultantAgenciesAndConsultingTypes'; +import { useConsultantRegistrationData } from './useConsultantRegistrationData'; import { ConsultingTypeRegistrationDefaults } from '../components/ProposedAgencies/ProposedAgencies'; import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; import { TopicsDataInterface } from '../../../globalState/interfaces/TopicsDataInterface'; @@ -28,30 +28,43 @@ export const useAgenciesForRegistration = ({ } => { const tenantData = useTenant(); + const { + consultant, + agency: preselectedAgency, + topic: preselectedTopic, + consultingType: preselectedConsultingType, + slugFallback + } = useContext(UrlParamsContext); + const { agencies: consultantAgencies, consultingTypes: consultantConsultingTypes - } = useConsultantAgenciesAndConsultingTypes(); + } = useConsultantRegistrationData(); const [isLoading, setIsLoading] = useState(true); const [agencies, setAgencies] = useState([]); - const { - consultant, - agency, - consultingType: preselectedConsultingType - } = useContext(UrlParamsContext); const { autoSelectPostcode, autoSelectAgency } = consultingType?.registration || ConsultingTypeRegistrationDefaults; + const topicsEnabledAndUnSelected = useMemo( + () => + tenantData?.settings?.featureTopicsEnabled && + tenantData?.settings?.topicsInRegistrationEnabled && + topic?.id === undefined, + [tenantData?.settings, topic?.id] + ); + const allAgencies = useMemo(() => { - // As long as no consulting type is selected we can't show any agencies - if (!consultingType) { + // As long as no consulting type or topic is selected we can't show any agencies + if (!consultingType || topicsEnabledAndUnSelected) { return []; } let uniqueAgencies = unionBy( - [agency, ...agencies, ...consultantAgencies].filter(Boolean), + [preselectedAgency, ...agencies, ...consultantAgencies].filter( + Boolean + ), 'id' ); @@ -61,22 +74,31 @@ export const useAgenciesForRegistration = ({ (agency) => !agency.external ); } - if (consultingType) { - uniqueAgencies = uniqueAgencies.filter( - (agency) => agency.consultingType === consultingType.id + + uniqueAgencies = uniqueAgencies + // Filter by preselected agency + + // Filter by consultingType + .filter( + (agency) => + slugFallback || + !consultingType || + agency.consultingType === consultingType.id ); - } + if (autoSelectAgency && uniqueAgencies.length > 0) { uniqueAgencies = [uniqueAgencies[0]]; } return uniqueAgencies; }, [ - agency, + preselectedAgency, + topicsEnabledAndUnSelected, agencies, consultantAgencies, consultingType, autoSelectPostcode, - autoSelectAgency + autoSelectAgency, + slugFallback ]); const allConsultingTypes = useMemo( @@ -95,13 +117,7 @@ export const useAgenciesForRegistration = ({ useEffect(() => { const abortController = new AbortController(); // if we already have information from consulting types we can ignore the request - if ( - consultant || - agency || - (tenantData?.settings?.featureTopicsEnabled && - tenantData?.settings?.topicsInRegistrationEnabled && - topic?.id === undefined) - ) { + if (consultant || preselectedAgency || topicsEnabledAndUnSelected) { setIsLoading(false); return; } @@ -111,7 +127,8 @@ export const useAgenciesForRegistration = ({ { postcode: autoSelectPostcode ? DEFAULT_POSTCODE : postcode, consultingType: consultingType?.id, - topicId: topic?.id + topicId: topic?.id, + fetchConsultingTypeDetails: true }, abortController.signal ) @@ -129,13 +146,13 @@ export const useAgenciesForRegistration = ({ abortController?.abort(); }; }, [ - agency, + preselectedAgency, autoSelectPostcode, consultant, consultingType?.id, topic?.id, postcode, - tenantData?.settings + topicsEnabledAndUnSelected ]); return { diff --git a/src/containers/registration/hooks/useConsultantAgenciesAndConsultingTypes.ts b/src/containers/registration/hooks/useConsultantAgenciesAndConsultingTypes.ts index 0cb4c7053..e69de29bb 100644 --- a/src/containers/registration/hooks/useConsultantAgenciesAndConsultingTypes.ts +++ b/src/containers/registration/hooks/useConsultantAgenciesAndConsultingTypes.ts @@ -1,79 +0,0 @@ -import { useState, useEffect, useContext } from 'react'; -import unionBy from 'lodash/unionBy'; - -import { - ConsultingTypeInterface, - AgencyDataInterface -} from '../../../globalState'; -import { useAppConfig } from '../../../hooks/useAppConfig'; -import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; - -export const useConsultantAgenciesAndConsultingTypes = () => { - const settings = useAppConfig(); - const { consultingType, consultant, agency } = useContext(UrlParamsContext); - - const [consultingTypes, setConsultingTypes] = useState< - ConsultingTypeInterface[] - >([]); - - const [agencies, setAgencies] = useState([]); - - useEffect(() => { - if (!consultant) { - return; - } - - // When we've the multi tenancy with single domain we can simply ignore the - // consulting types because we'll get agencies across tenants - // ToDo: This logic breaks consultant direct links with multiple consulting types - if ( - settings.multitenancyWithSingleDomainEnabled && - consultant?.agencies?.length > 0 - ) { - setAgencies(consultant?.agencies); - setConsultingTypes([consultingType]); - return; - } - - const consultingTypes = - // Remove consultingType duplicates - unionBy( - consultant.agencies.map( - ({ consultingTypeRel }) => consultingTypeRel - ), - 'id' - ) - // If consultingType was preselected by url slug - .filter((c) => !consultingType || c.id === consultingType.id); - - if (agency) { - const consultingTypeIds = consultingTypes.map((c) => c.id); - const preselectedAgency = consultant.agencies.find( - (a) => - a.id === agency.id && - consultingTypeIds.includes(a.consultingType) - ); - if (preselectedAgency) { - setAgencies([preselectedAgency]); - setConsultingTypes([preselectedAgency.consultingTypeRel]); - return; - } - } - - const possibleAgencies = consultant.agencies - // If a consultingType is selected filter the agencies - .filter((agency) => - consultingTypes.find((ct) => ct.id === agency.consultingType) - ); - - setAgencies(possibleAgencies); - setConsultingTypes(consultingTypes); - }, [ - consultant, - consultingType, - agency, - settings.multitenancyWithSingleDomainEnabled - ]); - - return { agencies, consultingTypes }; -}; diff --git a/src/containers/registration/hooks/useConsultantRegistrationData.ts b/src/containers/registration/hooks/useConsultantRegistrationData.ts new file mode 100644 index 000000000..c8cb288be --- /dev/null +++ b/src/containers/registration/hooks/useConsultantRegistrationData.ts @@ -0,0 +1,116 @@ +import { useState, useEffect, useContext, useMemo } from 'react'; +import unionBy from 'lodash/unionBy'; + +import { + ConsultingTypeInterface, + AgencyDataInterface, + TopicsDataInterface +} from '../../../globalState/interfaces'; +import { useAppConfig } from '../../../hooks/useAppConfig'; +import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; + +export const useConsultantRegistrationData = () => { + const settings = useAppConfig(); + const { + consultingType: preselectedConsultingType, + consultant, + agency: preselectedAgency, + topic: preselectedTopic, + slugFallback + } = useContext(UrlParamsContext); + + const [consultingTypes, setConsultingTypes] = useState< + ConsultingTypeInterface[] + >([]); + const [agencies, setAgencies] = useState([]); + + const topicIds = useMemo( + () => [ + ...new Set( + agencies + .reduce( + (topicIds, agency) => topicIds.concat(agency.topicIds), + [] + ) + // Filter topic by preselected topic + .filter( + (tid) => + !preselectedTopic || tid === preselectedTopic?.id + ) + // Filter topics by consultant topics + .filter( + (tid) => + !consultant || + consultant.agencies.some((a) => + a.topicIds?.includes(tid) + ) + ) + // Filter topics by preselected agency + .filter( + (tid) => + !preselectedAgency || + preselectedAgency.topicIds?.includes(tid) + ) + ) + ], + [agencies] + ); + + useEffect(() => { + if (!consultant) { + return; + } + + const consultingTypes = + // Remove consultingType duplicates + unionBy( + consultant.agencies.map( + ({ consultingTypeRel }) => consultingTypeRel + ), + 'id' + ) + // Filter consultingTypes by preselected consultingType + .filter( + (c) => + !preselectedConsultingType || + (slugFallback + ? c.slug === slugFallback + : c.id === preselectedConsultingType.id) + ); + if (preselectedAgency) { + setAgencies([preselectedAgency]); + setConsultingTypes([preselectedAgency.consultingTypeRel]); + return; + } + + const consultingTypeIds = consultingTypes.map((c) => c.id); + const possibleAgencies = consultant.agencies + // If a consultingType or topic is selected filter the agencies + .filter((agency) => + consultingTypeIds.includes(agency.consultingType) + ) + // Filter agencies by preselected topic + .filter( + (a) => + !preselectedTopic || + a.topicIds.includes(preselectedTopic?.id) + ) + // Filter agencies by preselected agency + .filter((a) => !preselectedAgency || preselectedAgency.id === a.id); + setAgencies(possibleAgencies); + setConsultingTypes( + slugFallback + ? [consultingTypes.pop()].filter(Boolean) + : consultingTypes + ); + }, [ + consultant, + preselectedConsultingType, + preselectedAgency, + preselectedTopic, + settings.multitenancyWithSingleDomainEnabled, + slugFallback + ]); + + return { agencies, consultingTypes, topicIds }; +}; diff --git a/src/globalState/interfaces/AppConfig/AppConfigInterface.ts b/src/globalState/interfaces/AppConfig/AppConfigInterface.ts index 880bcac39..c62399257 100644 --- a/src/globalState/interfaces/AppConfig/AppConfigInterface.ts +++ b/src/globalState/interfaces/AppConfig/AppConfigInterface.ts @@ -31,11 +31,7 @@ export interface AppConfigInterface extends AppSettingsInterface { }; groupChat?: GroupChatConfig; registration?: { - directlink?: { - fallbackLoader?: { - enabled?: boolean; - }; - }; + useConsultingTypeSlug?: boolean; }; } diff --git a/src/globalState/interfaces/UserDataInterface.ts b/src/globalState/interfaces/UserDataInterface.ts index 1bd2d0112..944d03496 100644 --- a/src/globalState/interfaces/UserDataInterface.ts +++ b/src/globalState/interfaces/UserDataInterface.ts @@ -35,9 +35,7 @@ export interface UserDataInterface { export interface ConsultantDataInterface extends Omit { consultantId: string; - agencies: (AgencyDataInterface & { - consultingTypeRel?: ConsultingTypeInterface; - })[]; + agencies: AgencyDataInterface[]; } export interface AgencyDataInterface { @@ -53,6 +51,8 @@ export interface AgencyDataInterface { external?: boolean; tenantId?: number; agencySpecificPrivacy?: string; + consultingTypeRel?: ConsultingTypeInterface; + topicIds?: number[]; } export interface ConsultingTypeDataInterface { diff --git a/src/globalState/provider/UrlParamsProvider.tsx b/src/globalState/provider/UrlParamsProvider.tsx index faa7cd84d..720d20f9d 100644 --- a/src/globalState/provider/UrlParamsProvider.tsx +++ b/src/globalState/provider/UrlParamsProvider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, FC } from 'react'; +import React, { createContext, PropsWithChildren, useMemo } from 'react'; import { AgencyDataInterface, ConsultantDataInterface, @@ -13,22 +13,34 @@ export const UrlParamsContext = createContext<{ consultant: ConsultantDataInterface | null; topic: TopicsDataInterface | null; loaded: boolean; + slugFallback: string; }>({ agency: null, consultingType: null, consultant: null, topic: null, - loaded: false + loaded: false, + slugFallback: undefined }); -export const UrlParamsProvider: FC = ({ children }) => { - const { agency, consultingType, consultant, topic, loaded } = +export const UrlParamsProvider = ({ children }: PropsWithChildren<{}>) => { + const { agency, consultingType, consultant, topic, loaded, slugFallback } = useUrlParamsLoader(); + const context = useMemo( + () => ({ + agency, + consultingType, + consultant, + topic, + loaded, + slugFallback + }), + [agency, consultingType, consultant, topic, loaded, slugFallback] + ); + return ( - + {children} ); diff --git a/src/utils/loadConsultingTypeForAgency.ts b/src/utils/loadConsultingTypeForAgency.ts new file mode 100644 index 000000000..8d605db53 --- /dev/null +++ b/src/utils/loadConsultingTypeForAgency.ts @@ -0,0 +1,11 @@ +import { apiGetConsultingType } from '../api'; +import { AgencyDataInterface } from '../globalState/interfaces'; + +export const loadConsultingTypeForAgency = async ( + agency: AgencyDataInterface +): Promise => ({ + ...agency, + consultingTypeRel: await apiGetConsultingType({ + consultingTypeId: agency?.consultingType + }) +}); diff --git a/src/utils/useUrlParamsLoader.tsx b/src/utils/useUrlParamsLoader.tsx index 1cb38401f..8d9ca86b0 100644 --- a/src/utils/useUrlParamsLoader.tsx +++ b/src/utils/useUrlParamsLoader.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useContext } from 'react'; +import { useState, useEffect, useContext, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { getUrlParameter } from './getUrlParameter'; import { @@ -34,15 +34,127 @@ export default function useUrlParamsLoader() { useState(null); const [loaded, setLoaded] = useState(false); const [topic, setTopic] = useState(null); + const [slugFallback, setSlugFallback] = useState(); + + const loadTopic = useCallback( + async (agency) => { + let topic = null; + + if (topicIdOrName === '') { + return [agency, topic]; + } + + if (isNumber(topicIdOrName)) { + topic = await apiGetTopicById(topicIdOrName).catch(() => null); + } else if (isString(topicIdOrName)) { + topic = await apiGetTopicsData() + .then( + (allTopics) => + allTopics.find( + (topic) => + topic.name?.toLowerCase() === + decodeURIComponent( + topicIdOrName.toLowerCase() + ) + ) || null + ) + .catch(() => null); + } + + if (!topic || !agency) { + return [agency, topic]; + } + + // If agency is preselected but did not fit the topic preselection set it to null + if (!agency.topicIds.includes(topic.id)) { + return [null, topic]; + } + + return [agency, topic]; + }, + [topicIdOrName] + ); + + const handleConsultant = useCallback( + async (agency, consultingType, topic) => { + if (!consultantId) { + return [agency, consultingType, topic, null]; + } + + const consultant = await apiGetConsultant(consultantId, true).catch( + () => { + // consultant not found -> go to registration + document.location.href = settings.urls.toRegistration; + } + ); + + if (!consultant) { + return [agency, consultingType, topic, null]; + } + + // If the agency does not match the consultant's agency, set the agency to null + if (!consultant.agencies.some((a) => a.id === agency?.id)) { + agency = null; + } + + // If the topic does not match the consultant's agency topics, set the topic to null + if ( + !consultant.agencies.some((a) => a.topicIds.includes(topic?.id)) + ) { + topic = null; + } + + // If the consultant agency consulting types does not match the consulting type, we'll set the consulting type to null + // If the agency is invalid and set to null already the consulting type was loaded by the agency. If the consultant + // has switched to another agency with the same consulting type this will not be catched by this conditions + // and the consulting type will be kept and only agencies from the consultant with the same consulting type will be shown + // but this should be fine. + + // Fallback logic for special client because slug is not unique. So try reversed logic + if ( + settings?.registration?.useConsultingTypeSlug && + consultingTypeSlug + ) { + setSlugFallback(consultingTypeSlug); + const slugAgencies = consultant.agencies.filter( + (a) => a.consultingTypeRel.slug === consultingTypeSlug + ); + if (slugAgencies.length > 0) { + consultingType = slugAgencies[0].consultingTypeRel; + } + } else if ( + // If the consultingType does not match the consultant's consultingTypes, set the consultingType to null + !consultant.agencies.some( + (a) => + !consultingType || + a.consultingType === consultingType?.id + ) + ) { + consultingType = null; + } + + return [agency, consultingType, topic, consultant]; + }, + [ + consultantId, + consultingTypeSlug, + settings?.registration?.useConsultingTypeSlug, + settings.urls.toRegistration + ] + ); useEffect(() => { (async () => { try { - let agency, - consultingType = null; + let agency: AgencyDataInterface = null, + consultingType: ConsultingTypeInterface = null, + consultant: ConsultantDataInterface = null, + topic: TopicsDataInterface = null; if (isNumber(agencyId)) { - agency = await apiGetAgencyById(agencyId).catch(() => null); + agency = await apiGetAgencyById(agencyId, true).catch( + () => null + ); } if (consultingTypeSlug || agency) { @@ -50,100 +162,28 @@ export default function useUrlParamsLoader() { consultingTypeSlug, consultingTypeId: agency?.consultingType }); - } - - if (consultantId) { - const consultant = await apiGetConsultant( - consultantId, - true, - 'basic' - ).catch(() => { - // consultant not found -> go to registration - document.location.href = settings.urls.toRegistration; - }); - if (consultant) { - setConsultant(consultant); - - // If the agency does not match the consultant's agency, we'll set the agency to null - if ( - !consultant.agencies.some( - (a) => a.id === agency?.id - ) - ) { - agency = null; - } - - // If the consultant agency consulting types does not match the consulting type, we'll set the consulting type to null - // If the agency is invalid and set to null already the consulting type was loaded by the agency. If the consultant - // has switched to another agency with the same consulting type this will not be catched by this conditions - // and the consulting type will be kept and only agencies from the consultant with the same consulting type will be shown - // but this should be fine. - if ( - !consultant.agencies.some( - (a) => - !consultingType || - a.consultingType === consultingType?.id - ) - ) { - consultingType = null; - - // Fallback logic for special client because slug is not unique. So try reversed logicc - if ( - settings?.registration?.directlink - ?.fallbackLoader?.enabled && - consultingTypeSlug - ) { - const loadConsultingType = async (id) => { - const ct = await apiGetConsultingType({ - consultingTypeId: id - }); - return ct.slug === consultingTypeSlug - ? ct - : null; - }; - - for (const { - consultingType: ctId - } of consultant.agencies) { - const res = await loadConsultingType(ctId); - if (res) { - consultingType = res; - break; - } - } - } - } + // Fallback logic for special client because slug is not unique. So try reversed logic + if ( + settings?.registration?.useConsultingTypeSlug && + consultingType + ) { + setSlugFallback(consultingType.slug); + } else if ( + agency?.consultingType !== consultingType?.id || + (agency?.external && + !consultingType?.registration?.autoSelectPostcode) + ) { + agency = null; } } - // When we've the multi tenancy with single domain enabled we'll always have multiple consulting types - if ( - !settings.multitenancyWithSingleDomainEnabled && - agency?.consultingType !== consultingType?.id - ) { - agency = null; - } - - if (isNumber(topicIdOrName) && topicIdOrName !== '') { - await apiGetTopicById(topicIdOrName) - .then(setTopic) - .catch(() => null); - } else if (isString(topicIdOrName) && topicIdOrName !== '') { - await apiGetTopicsData() - .then((allTopics) => { - const topic = allTopics.find( - (topic) => - topic.name?.toLowerCase() === - decodeURIComponent( - topicIdOrName.toLowerCase() - ) - ); - setTopic(topic); - }) - .catch(() => null); - } + [agency, topic] = await loadTopic(agency); + [agency, consultingType, topic, consultant] = + await handleConsultant(agency, consultingType, topic); + setTopic(topic); + setConsultant(consultant); setConsultingType(consultingType); setAgency(agency); setLoaded(true); @@ -158,7 +198,9 @@ export default function useUrlParamsLoader() { topicIdOrName, settings.multitenancyWithSingleDomainEnabled, settings.urls.toRegistration, - settings?.registration?.directlink?.fallbackLoader?.enabled + settings?.registration?.useConsultingTypeSlug, + handleConsultant, + loadTopic ]); useEffect(() => { @@ -167,5 +209,5 @@ export default function useUrlParamsLoader() { } }, [language, setLocale]); - return { agency, consultant, consultingType, loaded, topic }; + return { agency, consultant, consultingType, loaded, topic, slugFallback }; }