diff --git a/src/components/ProfileType/ProfileTypeFinished.tsx b/src/components/ProfileType/ProfileTypeFinished.tsx index 57d208a83342ae7e2d90b7cffeec905f1a015022..2e7cd4b2ab35e80dacfd93e9b0998280b2058fd1 100644 --- a/src/components/ProfileType/ProfileTypeFinished.tsx +++ b/src/components/ProfileType/ProfileTypeFinished.tsx @@ -4,8 +4,9 @@ import finishIcon from 'assets/icons/visu/profileType/finish.svg' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' import useExploration from 'components/Hooks/useExploration' import 'components/ProfileType/profileTypeFinished.scss' -import { useClient } from 'cozy-client' +import { Client, useClient } from 'cozy-client' import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import { PROFILETYPE_DOCTYPE } from 'doctypes' import { UsageEventType } from 'enum/usageEvent.enum' import { UserExplorationID } from 'enum/userExploration.enum' import { DateTime } from 'luxon' @@ -19,7 +20,7 @@ import ProfileTypeEntityService from 'services/profileTypeEntity.service' import UsageEventService from 'services/usageEvent.service' import { AppActionsTypes, AppStore } from 'store' import { updateProfile } from 'store/profile/profile.actions' -import { newProfileTypeEntry } from 'store/profileType/profileType.actions' +import { setProfileType } from 'store/profileType/profileType.slice' const ProfileTypeFinished: React.FC<{ profileType: ProfileType }> = ({ profileType, @@ -64,7 +65,7 @@ const ProfileTypeFinished: React.FC<{ profileType: ProfileType }> = ({ myProfileTypes ) if (destroyPT) { - dispatch(newProfileTypeEntry(consistentProfileType)) + await createNewProfileType(client, consistentProfileType) setIsSaved(true) dispatch( updateProfile({ @@ -77,7 +78,7 @@ const ProfileTypeFinished: React.FC<{ profileType: ProfileType }> = ({ Sentry.captureException('error in profileTypeFinished') } } else { - dispatch(newProfileTypeEntry(consistentProfileType)) + await createNewProfileType(client, consistentProfileType) setIsSaved(true) dispatch( updateProfile({ @@ -88,6 +89,20 @@ const ProfileTypeFinished: React.FC<{ profileType: ProfileType }> = ({ } } + async function createNewProfileType( + client: Client, + consistentProfileType: ProfileType + ) { + const { data: newProfileType } = await client.create( + PROFILETYPE_DOCTYPE, + consistentProfileType + ) + + if (newProfileType) { + dispatch(setProfileType(newProfileType)) + } + } + if (!isSaved) { checkForExistingProfileType() if ( diff --git a/src/components/ProfileType/ProfileTypeFormDateSelection.tsx b/src/components/ProfileType/ProfileTypeFormDateSelection.tsx index 025d2993252d4288bc18949b496b082a6db77016..59186b342d46dc8799d83fab1540030926596843 100644 --- a/src/components/ProfileType/ProfileTypeFormDateSelection.tsx +++ b/src/components/ProfileType/ProfileTypeFormDateSelection.tsx @@ -141,8 +141,7 @@ const ProfileTypeFormDateSelection: React.FC< }, [profileType, setPreviousStep]) const handleNext = useCallback(() => { - profileType[answerType.attribute] = answer - setNextStep(profileType) + setNextStep({ ...profileType, [answerType.attribute]: answer }) }, [profileType, setNextStep, answer, answerType.attribute]) function handleSelectMonth(event: any) { diff --git a/src/components/ProfileType/ProfileTypeFormMultiChoice.tsx b/src/components/ProfileType/ProfileTypeFormMultiChoice.tsx index f1210185c28b2880619f23c54be80c1ef5617264..46cec7784e22ae7d7284917d0bc49088137e3914 100644 --- a/src/components/ProfileType/ProfileTypeFormMultiChoice.tsx +++ b/src/components/ProfileType/ProfileTypeFormMultiChoice.tsx @@ -67,8 +67,10 @@ const ProfileTypeFormMultiChoice: React.FC<ProfileTypeFormMultiChoiceProps> = ({ }, [profileType, setPreviousStep]) const handleNext = useCallback(() => { - profileType[answerType.attribute] = answer as IndividualInsulationWork[] - setNextStep(profileType) + setNextStep({ + ...profileType, + [answerType.attribute]: answer as IndividualInsulationWork[], + }) }, [profileType, setNextStep, answer, answerType.attribute]) useEffect(() => { diff --git a/src/components/ProfileType/ProfileTypeFormNumber.tsx b/src/components/ProfileType/ProfileTypeFormNumber.tsx index 66e36f2bbe30b49ace38ddbafbedf5e20b75e2c4..c46ac1924aa835385cdc270112a35c7b8305b79d 100644 --- a/src/components/ProfileType/ProfileTypeFormNumber.tsx +++ b/src/components/ProfileType/ProfileTypeFormNumber.tsx @@ -37,8 +37,7 @@ const ProfileTypeFormNumber: React.FC<ProfileTypeFormNumberProps> = ({ }, [profileType, setPreviousStep]) const handleNext = useCallback(() => { - profileType[answerType.attribute] = answer - setNextStep(profileType) + setNextStep({ ...profileType, [answerType.attribute]: answer }) }, [profileType, setNextStep, answer, answerType.attribute]) useEffect(() => { diff --git a/src/components/ProfileType/ProfileTypeFormNumberSelection.tsx b/src/components/ProfileType/ProfileTypeFormNumberSelection.tsx index 944a82388a0830a1ffb40ae00c6255544e3455e9..d40309fa13cd78ab5b95cb5a6b94498a3b323cb7 100644 --- a/src/components/ProfileType/ProfileTypeFormNumberSelection.tsx +++ b/src/components/ProfileType/ProfileTypeFormNumberSelection.tsx @@ -51,8 +51,7 @@ const ProfileTypeFormNumberSelection: React.FC< }, [profileType, setPreviousStep]) const handleNext = useCallback(() => { - profileType[answerType.attribute] = answer - setNextStep(profileType) + setNextStep({ ...profileType, [answerType.attribute]: answer }) }, [profileType, setNextStep, answer, answerType.attribute]) useEffect(() => { diff --git a/src/components/ProfileType/ProfileTypeFormSingleChoice.tsx b/src/components/ProfileType/ProfileTypeFormSingleChoice.tsx index a4fc88272aa457a315d9bbdcc5b450affbe58552..f0fd6681cd7edd02886f21381b08addf581599ff 100644 --- a/src/components/ProfileType/ProfileTypeFormSingleChoice.tsx +++ b/src/components/ProfileType/ProfileTypeFormSingleChoice.tsx @@ -40,8 +40,7 @@ const ProfileTypeFormSingleChoice: React.FC< }, [profileType, setPreviousStep]) const handleNext = useCallback(() => { - profileType[answerType.attribute] = answer - setNextStep(profileType) + setNextStep({ ...profileType, [answerType.attribute]: answer }) }, [profileType, setNextStep, answer, answerType.attribute]) useEffect(() => { diff --git a/src/components/ProfileType/ProfileTypeView.tsx b/src/components/ProfileType/ProfileTypeView.tsx index 5fb0f3b2f6cec6fada99365f8df9ecee08d8154d..27c43500d59ef43272afc818a1b48828a8237d6b 100644 --- a/src/components/ProfileType/ProfileTypeView.tsx +++ b/src/components/ProfileType/ProfileTypeView.tsx @@ -115,17 +115,13 @@ const ProfileTypeView: React.FC = () => { ] ) - const setPreviousStep = useCallback( - (_profileType: ProfileType) => { - setProfileType(_profileType) - const profileTypeFormService = new ProfileTypeFormService(_profileType) - const previousStep: ProfileTypeStepForm = - profileTypeFormService.getPreviousFormStep(step) - setIsLoading(true) - setStep(previousStep) - }, - [step] - ) + const setPreviousStep = useCallback(() => { + const profileTypeFormService = new ProfileTypeFormService(profileType) + const previousStep: ProfileTypeStepForm = + profileTypeFormService.getPreviousFormStep(step) + setIsLoading(true) + setStep(previousStep) + }, [profileType, step]) const selectForm = () => { if (answerType.type === ProfileTypeFormType.SINGLE_CHOICE) { @@ -201,14 +197,17 @@ const ProfileTypeView: React.FC = () => { } useEffect(() => { - if (profile.isProfileTypeCompleted) { - setProfileType(curProfileType) - } const _answerType: ProfileTypeAnswer = ProfileTypeFormService.getAnswerForStep(step) setAnswerType(_answerType) setIsLoading(false) - }, [step, profile, curProfileType]) + }, [step]) + + useEffect(() => { + if (profile.isProfileTypeCompleted) { + setProfileType({ ...curProfileType }) + } + }, [curProfileType, profile]) return ( <> diff --git a/src/components/Splash/SplashRoot.tsx b/src/components/Splash/SplashRoot.tsx index 69efd04518d0a0b4aaeab98845f41bde7819d889..a486f8ae2d3271ce11c31668e91e32a53e1d3141 100644 --- a/src/components/Splash/SplashRoot.tsx +++ b/src/components/Splash/SplashRoot.tsx @@ -19,6 +19,7 @@ import { InitStepsErrors, PartnersInfo, Profile, + ProfileType, UserChallenge, } from 'models' import React, { @@ -35,6 +36,7 @@ import CustomPopupService from 'services/customPopup.service' import FluidService from 'services/fluid.service' import InitializationService from 'services/initialization.service' import PartnersInfoService from 'services/partnersInfo.service' +import ProfileTypeEntityService from 'services/profileTypeEntity.service' import UsageEventService from 'services/usageEvent.service' import { AppActionsTypes } from 'store' import { @@ -55,7 +57,7 @@ import { import { openPartnersModal, setCustomPopup } from 'store/modal/modal.slice' import { updateProfile } from 'store/profile/profile.actions' import { updateProfileEcogestureSuccess } from 'store/profileEcogesture/profileEcogesture.actions' -import { updateProfileType } from 'store/profileType/profileType.actions' +import { setProfileType } from 'store/profileType/profileType.slice' import { logDuration } from 'utils/duration' import logApp from 'utils/logger' import { getTodayDate } from 'utils/utils' @@ -164,6 +166,19 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { [dispatch, getPartnerStatus] ) + const loadProfileType = useCallback( + async (profileType: ProfileType) => { + const profileTypeEntityService = new ProfileTypeEntityService(client) + const updatedProfileType = await profileTypeEntityService.saveProfileType( + profileType + ) + if (updatedProfileType) { + dispatch(setProfileType(updatedProfileType)) + } + }, + [client, dispatch] + ) + useEffect(() => { let timeoutSplash: NodeJS.Timeout if (splashStart) { @@ -237,7 +252,7 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { } dispatch(updateProfile(updatedProfile)) if (profileType) { - dispatch(updateProfileType(profileType)) + await loadProfileType(profileType) } if (profileEcogesture) { dispatch(updateProfileEcogestureSuccess(profileEcogesture)) @@ -371,6 +386,7 @@ const SplashRoot = ({ fadeTimer = 1000, children }: SplashRootProps) => { client, dispatch, initStepErrors, + loadProfileType, processCustomPopup, processFluidsStatus, processPartnersStatus, diff --git a/src/services/profileTypeEntity.service.ts b/src/services/profileTypeEntity.service.ts index bcfa84e2d85a4ba6429c9a3b1ffd4acbc4bf5644..b436858f0d00fa5bb5633b008c238baf84c0884d 100644 --- a/src/services/profileTypeEntity.service.ts +++ b/src/services/profileTypeEntity.service.ts @@ -131,7 +131,7 @@ export default class ProfileTypeEntityService { * Saves profileType in database * @returns {ProfileType} */ - public async updateProfileType( + public async saveProfileType( attributes: Partial<ProfileType> ): Promise<ProfileType | null> { const query: QueryDefinition = Q(PROFILETYPE_DOCTYPE) diff --git a/src/store/index.ts b/src/store/index.ts index b7de1bbdce97b8ca092ce99920ed5600f5483fe2..3f16e08ff6fc7a77456555af1c0693a8a4c99664 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -30,8 +30,10 @@ import { ProfileActionTypes } from './profile/profile.actions' import { profileReducer } from './profile/profile.reducer' import { ProfileEcogestureActionTypes } from './profileEcogesture/profileEcogesture.actions' import { profileEcogestureReducer } from './profileEcogesture/profileEcogesture.reducer' -import { ProfileTypeActionTypes } from './profileType/profileType.actions' -import { profileTypeReducer } from './profileType/profileType.reducer' +import { + ProfileTypeActionTypes, + profileTypeSlice, +} from './profileType/profileType.slice' export interface EcolyoState { analysis: AnalysisState @@ -54,7 +56,7 @@ const ecolyoReducer = combineReducers({ modal: modalSlice.reducer, profile: profileReducer, profileEcogesture: profileEcogestureReducer, - profileType: profileTypeReducer, + profileType: profileTypeSlice.reducer, }) export interface AppStore { diff --git a/src/store/profileType/profileType.actions.spec.ts b/src/store/profileType/profileType.actions.spec.ts deleted file mode 100644 index dec9b8345c6ab2e56ecb080238b010d15bb12069..0000000000000000000000000000000000000000 --- a/src/store/profileType/profileType.actions.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { profileTypeData } from '../../../tests/__mocks__/profileType.mock' -import { createMockEcolyoStore } from '../../../tests/__mocks__/store' -import { updateProfileType, UPDATE_PROFILETYPE } from './profileType.actions' - -const mockUpdateProfileType = jest.fn() -jest.mock('services/profileTypeEntity.service', () => { - return jest.fn(() => { - return { - updateProfileType: mockUpdateProfileType, - } - }) -}) - -describe('profileType actions', () => { - const store = createMockEcolyoStore() - beforeEach(() => { - store.clearActions() - }) - it('should create an UPDATE_PROFILETYPE action when profileType is updated', async () => { - mockUpdateProfileType.mockResolvedValueOnce(profileTypeData) - const expectedActions = [ - { - type: UPDATE_PROFILETYPE, - payload: profileTypeData, - }, - ] - await store.dispatch(updateProfileType(profileTypeData)) - expect(store.getActions()).toEqual(expectedActions) - }) - - it('should not create action when profileType is not updated', async () => { - mockUpdateProfileType.mockResolvedValueOnce(null) - await store.dispatch(updateProfileType(profileTypeData)) - expect(store.getActions()).toEqual([]) - }) -}) diff --git a/src/store/profileType/profileType.actions.ts b/src/store/profileType/profileType.actions.ts deleted file mode 100644 index 25c43810920ec723e4a7adeb3ea7d6b6c053cc7b..0000000000000000000000000000000000000000 --- a/src/store/profileType/profileType.actions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Client } from 'cozy-client' -import { PROFILETYPE_DOCTYPE } from 'doctypes' -import { ProfileType } from 'models' -import { Dispatch } from 'react' -import ProfileTypeEntityService from 'services/profileTypeEntity.service' -import { AppStore, defaultAction } from 'store' - -export const UPDATE_PROFILETYPE = 'UPDATE_PROFILETYPE' -export const CREATE_NEW_PROFILETYPE = 'CREATE_NEW_PROFILETYPE' - -export interface UpdateProfileType { - type: typeof UPDATE_PROFILETYPE - payload?: ProfileType -} - -export interface CreateNewProfileType { - type: typeof CREATE_NEW_PROFILETYPE - payload?: ProfileType -} - -export type ProfileTypeActionTypes = - | UpdateProfileType - | CreateNewProfileType - | typeof defaultAction - -export function updateProfileTypeSuccess( - updatedProfileType: ProfileType -): UpdateProfileType { - return { - type: UPDATE_PROFILETYPE, - payload: updatedProfileType, - } -} - -export function updateProfileType(updates: Partial<ProfileType>): any { - return async ( - dispatch: Dispatch<UpdateProfileType>, - getState: () => AppStore, - { client }: { client: Client } - ): Promise<void> => { - const profileTypeEntityService = new ProfileTypeEntityService(client) - const updatedProfileType = await profileTypeEntityService.updateProfileType( - updates - ) - if (updatedProfileType) { - dispatch(updateProfileTypeSuccess(updatedProfileType)) - } - } -} - -export function newProfileTypeEntry(updates: Partial<ProfileType>): any { - return async ( - dispatch: Dispatch<UpdateProfileType>, - getState: () => AppStore, - { client }: { client: Client } - ) => { - const { data: newProfileType } = await client.create( - PROFILETYPE_DOCTYPE, - updates - ) - if (newProfileType) { - dispatch(updateProfileTypeSuccess(newProfileType)) - } - } -} diff --git a/src/store/profileType/profileType.reducer.spec.ts b/src/store/profileType/profileType.reducer.spec.ts deleted file mode 100644 index 7704a227a54f9cfa5f719fa517a8524234b41600..0000000000000000000000000000000000000000 --- a/src/store/profileType/profileType.reducer.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defaultAction } from 'store' -import { profileTypeData } from '../../../tests/__mocks__/profileType.mock' -import { mockInitialProfileTypeState } from '../../../tests/__mocks__/store' -import { UPDATE_PROFILETYPE } from './profileType.actions' -import { profileTypeReducer } from './profileType.reducer' - -describe('profile reducer', () => { - it('should return the initial state', () => { - const state = profileTypeReducer(undefined, { ...defaultAction }) - expect(state).toEqual(mockInitialProfileTypeState) - }) - - it('should handle UPDATE_PROFILETYPE with payload', () => { - const state = profileTypeReducer(mockInitialProfileTypeState, { - type: UPDATE_PROFILETYPE, - payload: profileTypeData, - }) - expect(state).toEqual(profileTypeData) - }) - - it('should handle UPDATE_PROFILETYPE without payload', () => { - const state = profileTypeReducer(mockInitialProfileTypeState, { - type: UPDATE_PROFILETYPE, - }) - expect(state).toEqual(mockInitialProfileTypeState) - }) -}) diff --git a/src/store/profileType/profileType.slice.spec.ts b/src/store/profileType/profileType.slice.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4fb708b43e73f4cf39bffc4496c6cc09f015718 --- /dev/null +++ b/src/store/profileType/profileType.slice.spec.ts @@ -0,0 +1,67 @@ +import { EquipmentType } from 'enum/ecogesture.enum' +import { FluidType } from 'enum/fluid.enum' +import { + ConstructionYear, + Floor, + HotWaterEquipment, + HotWaterFluid, + HousingType, + IndividualOrCollective, + OutsideFacingWalls, + ThreeChoicesAnswer, + WarmingType, +} from 'enum/profileType.enum' +import { DateTime } from 'luxon' +import { ProfileType } from 'models' +import { mockInitialProfileTypeState } from '../../../tests/__mocks__/store' +import { profileTypeSlice, setProfileType } from './profileType.slice' + +describe('profileType reducer', () => { + it('should return the initial state', () => { + const initialState = profileTypeSlice.reducer(undefined, { + type: undefined, + }) + expect(initialState).toEqual(mockInitialProfileTypeState) + }) + + describe('setProfileType', () => { + it('should handle update with partial payload', () => { + const state = profileTypeSlice.reducer( + mockInitialProfileTypeState, + setProfileType({ housingType: HousingType.APARTMENT }) + ) + expect(state).toEqual({ + ...mockInitialProfileTypeState, + housingType: HousingType.APARTMENT, + }) + }) + + it('should handle update with full payload', () => { + const newProfileType: ProfileType = { + housingType: HousingType.APARTMENT, + constructionYear: ConstructionYear.AFTER_1998, + area: '200', + occupantsNumber: 8, + outsideFacingWalls: OutsideFacingWalls.ONE, + floor: Floor.INTERMEDIATE_FLOOR, + heating: IndividualOrCollective.INDIVIDUAL, + coldWater: IndividualOrCollective.INDIVIDUAL, + hotWater: IndividualOrCollective.INDIVIDUAL, + individualInsulationWork: [], + hasInstalledVentilation: ThreeChoicesAnswer.YES, + hasReplacedHeater: ThreeChoicesAnswer.YES, + hotWaterEquipment: HotWaterEquipment.THERMODYNAMIC, + warmingFluid: WarmingType.ELECTRICITY, + hotWaterFluid: HotWaterFluid.ELECTRICITY, + cookingFluid: FluidType.ELECTRICITY, + updateDate: DateTime.fromISO('0000-01-01T00:00:00.000Z'), + equipments: [EquipmentType.BOILER], + } + const state = profileTypeSlice.reducer( + mockInitialProfileTypeState, + setProfileType(newProfileType) + ) + expect(state).toEqual(newProfileType) + }) + }) +}) diff --git a/src/store/profileType/profileType.reducer.ts b/src/store/profileType/profileType.slice.ts similarity index 70% rename from src/store/profileType/profileType.reducer.ts rename to src/store/profileType/profileType.slice.ts index 6dbc015b193ab6f42cc01de3370510b193a0a646..ec4beee131b88a6874bcc00f1104cdd66409d613 100644 --- a/src/store/profileType/profileType.reducer.ts +++ b/src/store/profileType/profileType.slice.ts @@ -1,3 +1,4 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit' import { FluidType } from 'enum/fluid.enum' import { ConstructionYear, @@ -13,12 +14,6 @@ import { } from 'enum/profileType.enum' import { DateTime } from 'luxon' import { ProfileType } from 'models' -import { Reducer } from 'redux' -import { - CREATE_NEW_PROFILETYPE, - UPDATE_PROFILETYPE, -} from 'store/profileType/profileType.actions' -import { ProfileTypeActionTypes } from './profileType.actions' const initialState: ProfileType = { housingType: HousingType.INDIVIDUAL_HOUSE, @@ -44,18 +39,19 @@ const initialState: ProfileType = { equipments: [], } -export const profileTypeReducer: Reducer< - ProfileType, - ProfileTypeActionTypes -> = (state = initialState, action) => { - switch (action.type) { - case UPDATE_PROFILETYPE: - case CREATE_NEW_PROFILETYPE: - return { - ...state, - ...action.payload, - } - default: - return state - } -} +type SetProfileType = PayloadAction<Partial<ProfileType>> + +export type ProfileTypeActionTypes = SetProfileType + +export const profileTypeSlice = createSlice({ + name: 'profileType', + initialState, + reducers: { + setProfileType: (state, action: SetProfileType) => { + Object.assign(state, action.payload) + }, + }, +}) + +export const { setProfileType } = profileTypeSlice.actions +export default profileTypeSlice.reducer