Commit f6e9bec6 authored by Hugo NOUTS's avatar Hugo NOUTS
Browse files

feat(ProfileType): Handling profileTypes modifications over time

There can only be one profileType per month. A profileType can be active over many months.
ProfileTypes are now set on the 1st of the month. If a profileType is being completed for the first time, the profileType will be linked to the current month.
Before saving a new profileType, check for other occurences on the given time period. If occurences are found delete them, new profileType overrides them.
Introduce a new migration to make sure no multiple profileTypes can exist over the same month.
parent c7b20531
......@@ -76,7 +76,7 @@ const AnalysisConsumption: React.FC<AnalysisConsumptionProps> = ({
async function loadAverageComsumption() {
const profileTypeEntityService = new ProfileTypeEntityService(client)
const profileType: ProfileType | null = await profileTypeEntityService.getProfileType(
analysisDate.plus({ month: -1 })
analysisDate.minus({ month: 1 }).startOf('month')
)
if (profileType !== null) {
const profileTypeService: ProfileTypeService = new ProfileTypeService(
......
......@@ -12,10 +12,13 @@ import finishIcon from 'assets/icons/visu/profileType/finish.svg'
import ProfileTypeService from 'services/profileType.service'
import useExploration from 'components/Hooks/useExploration'
import { AppStore } from 'store'
import { TimePeriod } from 'models'
import { UserExplorationID } from 'enum/userExploration.enum'
import { UsageEventType } from 'enum/usageEvent.enum'
import { useClient } from 'cozy-client'
import UsageEventService from 'services/usageEvent.service'
import ProfileTypeEntityService from 'services/profileTypeEntity.service'
import { DateTime } from 'luxon'
interface ProfileTypeFinishedProps {
profileType: ProfileType
......@@ -37,13 +40,53 @@ const ProfileTypeFinished: React.FC<ProfileTypeFinishedProps> = ({
const [isSaved, setIsSaved] = useState<boolean>(false)
const [, setValidExploration] = useExploration()
const profile = useSelector((state: AppStore) => state.ecolyo.profile)
useEffect(() => {
if (!isSaved) {
const consistentProfileType = ProfileTypeService.checkConsistency(
async function checkForExistingProfileType() {
const consistentProfileType: ProfileType = ProfileTypeService.checkConsistency(
profileType
)
setIsSaved(true)
const chosenPeriod: TimePeriod = {
startDate: profileType.updateDate.setZone('utc', {
keepLocalTime: true,
}),
endDate: DateTime.local().setZone('utc', {
keepLocalTime: true,
}),
}
const profileTypeEntityService = new ProfileTypeEntityService(client)
const myProfileTypes:
| ProfileType[]
| null = await profileTypeEntityService.getAllProfileTypes(chosenPeriod)
if (myProfileTypes !== null) {
const destroyPT = await profileTypeEntityService.deleteProfileTypes(
myProfileTypes
)
if (destroyPT) {
dispatch(newProfileTypeEntry(consistentProfileType))
setIsSaved(true)
dispatch(
updateProfile({
isProfileTypeCompleted: true,
})
)
} else {
console.log('ERROR')
}
} else {
dispatch(newProfileTypeEntry(consistentProfileType))
setIsSaved(true)
dispatch(
updateProfile({
isProfileTypeCompleted: true,
})
)
}
}
if (!isSaved) {
checkForExistingProfileType()
if (
currentChallenge &&
currentChallenge.exploration.id === UserExplorationID.EXPLORATION001
......@@ -53,18 +96,12 @@ const ProfileTypeFinished: React.FC<ProfileTypeFinishedProps> = ({
UsageEventService.addEvent(client, {
type: UsageEventType.PROFILE_SET_EVENT,
})
dispatch(
updateProfile({
isProfileTypeCompleted: true,
})
)
dispatch(newProfileTypeEntry(consistentProfileType))
}
}, [
dispatch,
profileType,
isSaved,
profile.isProfileTypeCompleted,
currentChallenge,
setValidExploration,
client,
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useCallback, useEffect, useState } from 'react'
import 'components/ProfileType/profileTypeForm.scss'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import ProfileTypeProgress from 'components/ProfileType/ProfileTypeProgress'
import ProfileTypeNavigation from 'components/ProfileType/ProfileTypeNavigation'
import { ProfileTypeStepForm } from 'enum/profileType.enum'
import {
ProfileType,
ProfileTypeAnswer,
ProfileTypeAnswerChoices,
} from 'models/profileType.model'
import { DateTime } from 'luxon'
import { MenuItem, Select } from '@material-ui/core'
interface ProfileTypeFormDateSelectionProps {
step: ProfileTypeStepForm
viewedStep: ProfileTypeStepForm
profileType: ProfileType
answerType: ProfileTypeAnswer
setNextStep: Function
setPrevioustStep: Function
isProfileTypeComplete: boolean
}
interface SelectionMonth {
label: string
value: number
}
const ProfileTypeFormDateSelection: React.FC<ProfileTypeFormDateSelectionProps> = ({
step,
viewedStep,
profileType,
answerType,
setNextStep,
setPrevioustStep,
isProfileTypeComplete,
}: ProfileTypeFormDateSelectionProps) => {
const { t } = useI18n()
const [selectedMonth, setSelectedMonth] = useState<any>({
label: DateTime.now().toLocaleString({ month: 'long' }),
value: DateTime.now().month, // Date.getMonth starts at 0
})
const [selectedYear, setSelectedYear] = useState<number>(DateTime.now().year)
const [answer, setAnswer] = useState<ProfileTypeAnswerChoices>('')
const selectMonths = [
{
label: 'Janvier',
value: '01',
},
{
label: 'Février',
value: '02',
},
{
label: 'Mars',
value: '03',
},
{
label: 'Avril',
value: '04',
},
{
label: 'Mai',
value: '05',
},
{
label: 'Juin',
value: '06',
},
{
label: 'Juillet',
value: '07',
},
{
label: 'Aout',
value: '08',
},
{
label: 'Septembre',
value: '09',
},
{
label: 'Octobre',
value: '10',
},
{
label: 'Novembre',
value: '11',
},
{
label: 'Décembre',
value: '12',
},
]
const selectYears = []
const curYear = DateTime.now().year
const limitYears = curYear - 10
for (let i = curYear; i >= limitYears; i--) {
selectYears.push(i)
}
function getMonthFullName(month: number) {
switch (month) {
case 1:
return 'Janvier'
case 2:
return 'Février'
case 3:
return 'Mars'
case 4:
return 'Avril'
case 5:
return 'Mai'
case 6:
return 'Juin'
case 7:
return 'Juillet'
case 8:
return 'Aout'
case 9:
return 'Septembre'
case 10:
return 'Octobre'
case 11:
return 'Novembre'
case 12:
return 'Décembre'
default:
return null
}
}
const handlePrevious = useCallback(() => {
setPrevioustStep(profileType)
}, [profileType, setPrevioustStep])
const handleNext = useCallback(() => {
profileType[answerType.attribute] = answer
setNextStep(profileType)
}, [profileType, setNextStep, answer, answerType.attribute])
function handleSelectMonth(event: any) {
setSelectedMonth({
value: event.target.value,
label: getMonthFullName(parseInt(event.target.value)),
})
const isoString: string = selectedYear + '-' + selectedMonth.value + '-01'
setAnswer(DateTime.fromISO(isoString))
}
function handleSelectYear(event: any) {
setSelectedYear(parseInt(event.target.value))
const isoString: string = selectedYear + '-' + selectedMonth.value + '-01'
setAnswer(DateTime.fromISO(isoString))
}
useEffect(() => {
if (step < viewedStep || isProfileTypeComplete) {
const isoString: string = selectedYear + '-' + selectedMonth.value + '-01'
setAnswer(DateTime.fromISO(isoString))
profileType[answerType.attribute] = DateTime.fromISO(isoString)
}
}, [
step,
viewedStep,
profileType,
answerType,
isProfileTypeComplete,
selectedYear,
selectedMonth.value,
])
return (
<>
<div className={'profile-form-container'}>
<ProfileTypeProgress step={step} />
<div className={'profile-question-label'}>
{t(
`profile_type.${ProfileTypeStepForm[step].toLowerCase()}.question`
)}
</div>
{answer !== null ? (
<div className="select-container">
<div className="date-select">
<Select
native={false}
labelId="selectMonthDate"
className="month"
defaultValue={selectedMonth.value}
onChange={e => handleSelectMonth(e)}
>
{/* if current year, only show past and present months else show full months */}
{selectedYear === DateTime.now().year
? selectMonths
.slice(0, DateTime.now().month)
.map((month, key) => (
<MenuItem
value={month.value}
key={key}
className="date-option"
>
{month.label}
</MenuItem>
))
: selectMonths.map((month, key) => (
<MenuItem
value={month.value}
key={key}
className="date-option"
>
{month.label}
</MenuItem>
))}
</Select>
</div>
<div className="date-select">
<Select
labelId="selectYearDate"
className="year"
defaultValue={selectedYear}
onChange={e => handleSelectYear(e)}
>
{selectYears.map((year, key) => (
<MenuItem value={year} key={key} className="date-option">
{year}
</MenuItem>
))}
</Select>
</div>
</div>
) : null}
</div>
<ProfileTypeNavigation
step={step}
handlePrevious={handlePrevious}
handleNext={handleNext}
disableNextButton={answer === ''}
/>
</>
)
}
export default ProfileTypeFormDateSelection
......@@ -43,7 +43,7 @@ const ProfileTypeNavigation: React.FC<ProfileTypeNavigationProps> = ({
</Button>
<Button
aria-label={
step === ProfileTypeStepForm.COOKING_FLUID
step === ProfileTypeStepForm.UPDATE_DATE
? t('profile_type.accessibility.button_end')
: t('profile_type.accessibility.button_next')
}
......@@ -57,7 +57,7 @@ const ProfileTypeNavigation: React.FC<ProfileTypeNavigationProps> = ({
label: 'text-16-normal',
}}
>
{step === ProfileTypeStepForm.COOKING_FLUID
{step === ProfileTypeStepForm.UPDATE_DATE
? t('profile_type.form.button_end')
: `${t('profile_type.form.button_next')} >`}
</Button>
......
......@@ -21,14 +21,14 @@ import {
} from 'enum/profileType.enum'
import { FluidType } from 'enum/fluid.enum'
import { ProfileTypeStepForm } from 'enum/profileType.enum'
import ProfileTypeService from 'services/profileType.service'
import ProfileTypeFormMultiChoice from 'components/ProfileType/ProfileTypeFormMultiChoice'
import ProfileTypeFormNumber from 'components/ProfileType/ProfileTypeFormNumber'
import ProfileTypeFormNumberSelection from 'components/ProfileType/ProfileTypeFormNumberSelection'
import { useSelector } from 'react-redux'
import { AppStore } from 'store'
import { DateTime } from 'luxon'
import ProfileTypeFormService from 'services/profileTypeForm'
import ProfileTypeFormService from 'services/profileTypeForm.service'
import ProfileTypeFormDateSelection from './ProfileTypeFormDateSelection'
const ProfileTypeView = () => {
const profile = useSelector((state: AppStore) => state.ecolyo.profile)
......@@ -37,9 +37,11 @@ const ProfileTypeView = () => {
)
const [headerHeight, setHeaderHeight] = useState<number>(0)
const [profileType, setProfileType] = useState<ProfileType>({
updateDate: DateTime.local().setZone('utc', {
keepLocalTime: true,
}),
updateDate: DateTime.local()
.setZone('utc', {
keepLocalTime: true,
})
.startOf('month'),
housingType: HousingType.INDIVIDUAL_HOUSE,
constructionYear: ConstructionYear.BETWEEN_1975_AND_1989,
area: '0',
......@@ -65,6 +67,7 @@ const ProfileTypeView = () => {
attribute: '',
choices: [],
})
const [isLoading, setIsLoading] = useState<boolean>(true)
const [viewedStep, setViewedStep] = useState<number>(-1)
......@@ -77,7 +80,8 @@ const ProfileTypeView = () => {
setProfileType(_profileType)
const profileTypeFormService = new ProfileTypeFormService(_profileType)
const nextStep: ProfileTypeStepForm = profileTypeFormService.getNextFormStep(
step
step,
!profile.isProfileTypeCompleted
)
setIsLoading(true)
if (nextStep > viewedStep) {
......@@ -92,11 +96,11 @@ const ProfileTypeView = () => {
(_profileType: ProfileType) => {
setProfileType(_profileType)
const profileTypeFormService = new ProfileTypeFormService(_profileType)
const nextStep: ProfileTypeStepForm = profileTypeFormService.getPreviousFormStep(
const previousStep: ProfileTypeStepForm = profileTypeFormService.getPreviousFormStep(
step
)
setIsLoading(true)
setStep(nextStep)
setStep(previousStep)
},
[step]
)
......@@ -150,6 +154,18 @@ const ProfileTypeView = () => {
setPrevioustStep={setPrevioustStep}
/>
)
} else if (answerType.type === ProfileTypeFormType.DATE_SELECTION) {
return (
<ProfileTypeFormDateSelection
step={step}
viewedStep={viewedStep}
profileType={profileType}
answerType={answerType}
setNextStep={setNextStep}
isProfileTypeComplete={profile.isProfileTypeCompleted}
setPrevioustStep={setPrevioustStep}
/>
)
}
}
......@@ -157,7 +173,7 @@ const ProfileTypeView = () => {
if (profile.isProfileTypeCompleted) {
setProfileType(curProfileType)
}
const _answerType: ProfileTypeAnswer = ProfileTypeService.getAnswerForStep(
const _answerType: ProfileTypeAnswer = ProfileTypeFormService.getAnswerForStep(
step
)
setAnswerType(_answerType)
......
@import '../../styles/base/color';
@import '../../styles/base/breakpoint';
@import '../../styles/base/typo-variables';
.profile-form-container {
color: $white;
......@@ -152,3 +153,74 @@
button:disabled {
opacity: 0.5;
}
.date-select {
margin: 0.5em;
border: 1px solid $gold-shadow;
background: $dark-light-2;
font-weight: bold;
.MuiInput-underline:after {
display: none;
}
.year {
text-align: center;
display: inline-flex;
align-content: center;
font-size: 1.25rem;
font-family: $text-font;
color: $white;
width: 93px;
svg {
top: 0;
background-color: $gold-shadow;
height: 100%;
}
}
.month {
color: $white;
font-size: 1.25rem;
text-align: center;
display: inline-flex;
align-content: center;
font-family: $text-font;
min-width: 130px;
max-width: 150px;
svg {
top: 0;
right: 0;
background-color: $gold-shadow;
height: 100%;
}
}
}
.select-container {
display: flex;
}
.date-option {
color: $white;
}
ul {
background: linear-gradient(180deg, #323339 0%, #25262b 100%);
color: $white;
font-weight: normal;
.MuiMenuItem-root {
font-family: $text-font;
text-align: center;
font-size: 1.25rem;
display: flex;
justify-content: space-evenly;
:hover {
background-color: $gold-shadow;
}
}
.MuiListItem-root.Mui-selected,
.MuiListItem-root.Mui-selected:hover {
background-color: $gold-shadow;
color: $dark-2;
font-weight: bold;
display: flex;
justify-content: space-evenly;
}
}
......@@ -73,7 +73,8 @@ export enum ProfileTypeStepForm {
HOT_WATER_FLUID = 13,
HOT_WATER_EQUIPMENT = 14,
COOKING_FLUID = 15,
END = 16,
UPDATE_DATE = 16,
END = 17,
}
export enum ProfileTypeFormType {
......@@ -81,4 +82,5 @@ export enum ProfileTypeFormType {
MULTI_CHOICE = 1,
NUMBER_SELECTION = 2,
NUMBER = 3,
DATE_SELECTION = 4,
}
......@@ -853,6 +853,10 @@
"0": "Électricité",
"2": "Gaz"
},
"update_date": {
"title": "Date de prise d'effet",
"question": "A partir de quelle date souhaitez-vous que ce nouveau profil soit pris en compte dans l'analyse de vos données ?"
},
"fluidType": {
"0": "Électricité",
"1": "Eau",
......
......@@ -232,4 +232,36 @@ export const migrations: Migration[] = [
})
},
},
{
baseSchemaVersion: 7,
targetSchemaVersion: 8,
appVersion: '1.5.0',
description:
'ProfileTypes start now at the begining of the month, no duplications can exist over the same month.',
releaseNotes: null,
docTypes: PROFILETYPE_DOCTYPE,
run: async (_client: Client, docs: any[]): Promise<any[]> => {
function checkDate(d1, d2) {
const dtd1: DateTime = DateTime.fromISO(d1)
const dtd2: DateTime = DateTime.fromISO(d2)
return dtd1.year === dtd2.year && dtd1.month === dtd2.month
}
for (let i = 0; i < docs.length; i++) {
const dtStartOfMonth: DateTime = DateTime.fromISO(docs[i].updateDate)
docs[i].updateDate = dtStartOfMonth
.setZone('utc', {
keepLocalTime: true,
})
.startOf('month')
if (
docs[i + 1] &&
checkDate(docs[i].updateDate