diff --git a/src/assets/icons/ico/graph-icon.svg b/src/assets/icons/ico/graph-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..d392bc746e368594be464b33654e4f1ca23c02ae --- /dev/null +++ b/src/assets/icons/ico/graph-icon.svg @@ -0,0 +1,7 @@ +<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 29C0 26.7909 1.79086 25 4 25H6.75556C8.96469 25 10.7556 26.7909 10.7556 29V44H0V29Z" fill="#FFF1C5"/> +<path d="M0 29C0 26.7909 1.79086 25 4 25H6.75556C8.96469 25 10.7556 26.7909 10.7556 29V44H0V29Z" fill="#261C14" fill-opacity="0.65"/> +<path d="M16.6221 4C16.6221 1.79086 18.4129 0 20.6221 0H23.3776C25.5868 0 27.3776 1.79086 27.3776 4V44H16.6221V4Z" fill="#E3B82A"/> +<path d="M33.2446 17C33.2446 14.7909 35.0355 13 37.2446 13H40.0002C42.2093 13 44.0002 14.7909 44.0002 17V44H33.2446V17Z" fill="#FFF1C5"/> +<path d="M33.2446 17C33.2446 14.7909 35.0355 13 37.2446 13H40.0002C42.2093 13 44.0002 14.7909 44.0002 17V44H33.2446V17Z" fill="#261C14" fill-opacity="0.65"/> +</svg> diff --git a/src/components/Analysis/MaxConsumptionCard.spec.tsx b/src/components/Analysis/MaxConsumptionCard.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..580803a245262ae45d7e3e0e323483df97d20dad --- /dev/null +++ b/src/components/Analysis/MaxConsumptionCard.spec.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import { mount } from 'enzyme' + +import MonthlyAnalysis from 'components/Analysis/MonthlyAnalysis' +import { DateTime } from 'luxon' + +import * as reactRedux from 'react-redux' +import { userChallengeExplo1OnGoing } from '../../../tests/__mocks__/userChallengeData.mock' + +import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock' +import { Provider } from 'react-redux' +import configureStore from 'redux-mock-store' +import MaxConsumptionCard from './MaxConsumptionCard' +import { FluidType } from 'enum/fluid.enum' +import { baseDataLoad } from '../../../tests/__mocks__/datachartData.mock' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) +const mockStore = configureStore([]) +const mockUseSelector = jest.spyOn(reactRedux, 'useSelector') + +describe('MaxConsumptionCard component', () => { + it('should be rendered correctly', () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + }, + }) + mockUseSelector.mockReturnValue({ + fluidTypes: [FluidType.ELECTRICITY, FluidType.GAS], + }) + const wrapper = mount( + <Provider store={store}> + <MaxConsumptionCard + analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', { + zone: 'utc', + })} + /> + </Provider> + ).getElement() + expect(wrapper).toMatchSnapshot() + }) + it('should be rendered with one fluid and not display arrows', () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + }, + }) + mockUseSelector.mockReturnValue({ fluidTypes: [FluidType.ELECTRICITY] }) + const wrapper = mount( + <Provider store={store}> + <MaxConsumptionCard + analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', { + zone: 'utc', + })} + /> + </Provider> + ) + expect(wrapper.find('.arrow').exists()).toBeFalsy() + }) + it('should be rendered with several fluids and click navigate between fluid', async () => { + const store = mockStore({ + ecolyo: { + global: globalStateData, + }, + }) + mockUseSelector.mockReturnValue({ + fluidTypes: [FluidType.ELECTRICITY, FluidType.GAS], + }) + const wrapper = mount( + <Provider store={store}> + <MaxConsumptionCard + analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', { + zone: 'utc', + })} + /> + </Provider> + ) + expect(wrapper.find('.arrow-next').exists()).toBeTruthy() + //navigate next + wrapper + .find('.arrow-next') + .first() + .simulate('click') + expect(wrapper.find('.fluid').text()).toBe('FLUID.ELECTRICITY.LABEL') + wrapper + .find('.arrow-next') + .first() + .simulate('click') + expect(wrapper.find('.fluid').text()).toBe('FLUID.GAS.LABEL') + //navigate prev + wrapper + .find('.arrow-prev') + .first() + .simulate('click') + expect(wrapper.find('.fluid').text()).toBe('FLUID.ELECTRICITY.LABEL') + wrapper + .find('.arrow-prev') + .first() + .simulate('click') + expect(wrapper.find('.fluid').text()).toBe('FLUID.GAS.LABEL') + }) +}) diff --git a/src/components/Analysis/MaxConsumptionCard.tsx b/src/components/Analysis/MaxConsumptionCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4e8b9948c7c92c2312a0266511cb3fcb5e7737d2 --- /dev/null +++ b/src/components/Analysis/MaxConsumptionCard.tsx @@ -0,0 +1,159 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import LeftArrowIcon from 'assets/icons/ico/left-arrow.svg' +import RigthArrowIcon from 'assets/icons/ico/right-arrow.svg' +import StyledIcon from 'components/CommonKit/Icon/StyledIcon' +import IconButton from '@material-ui/core/IconButton' +import Icon from 'cozy-ui/transpiled/react/Icon' +import GraphIcon from 'assets/icons/ico/graph-icon.svg' +import { FluidType } from 'enum/fluid.enum' +import { useSelector } from 'react-redux' +import { AppStore } from 'store' +import { getNavPicto } from 'utils/picto' +import { DateTime } from 'luxon' +import { useClient } from 'cozy-client' +import { TimeStep } from 'enum/timeStep.enum' +import { Dataload, TimePeriod } from 'models' +import ConsumptionDataManager from 'services/consumption.service' +import './maxConsumptionCard.scss' +import StyledSpinner from 'components/CommonKit/Spinner/StyledSpinner' +import { formatNumberValues } from 'utils/utils' + +interface MaxConsumptionCardProps { + analysisDate: DateTime +} + +const MaxConsumptionCard: React.FC<MaxConsumptionCardProps> = ({ + analysisDate, +}: MaxConsumptionCardProps) => { + const { t } = useI18n() + const client = useClient() + + const { fluidTypes } = useSelector((state: AppStore) => state.ecolyo.global) + const [index, setIndex] = useState<number>(0) + const [currentFluid, setcurrentFluid] = useState<FluidType>(fluidTypes[index]) + const [maxDayData, setMaxDayData] = useState<Dataload | null>(null) + const [isLoading, setisLoading] = useState<boolean>(true) + + const handleChangePrevFluid = useCallback(() => { + setisLoading(true) + if (index === 0) { + setIndex(fluidTypes.length - 1) + } else { + setIndex(prev => prev - 1) + } + setcurrentFluid(fluidTypes[index]) + }, [fluidTypes, index]) + + const handleChangeNextFluid = useCallback(() => { + setisLoading(true) + if (index === fluidTypes.length - 1) { + setIndex(0) + } else { + setIndex(prev => prev + 1) + } + setcurrentFluid(fluidTypes[index]) + }, [fluidTypes, index]) + + useEffect(() => { + let subscribed = true + async function getMaxLoadData() { + const timePeriod: TimePeriod = { + startDate: analysisDate.minus({ month: 1 }).startOf('month'), + endDate: analysisDate.minus({ month: 1 }).endOf('month'), + } + const consumptionService = new ConsumptionDataManager(client) + const monthMaxData = await consumptionService.getMaxLoad( + timePeriod, + TimeStep.DAY, + [currentFluid], + undefined, + false, + true + ) + if (monthMaxData) { + setMaxDayData(monthMaxData as Dataload) + setisLoading(false) + } + } + if (subscribed) { + getMaxLoadData() + } + return () => { + subscribed = false + } + }, [analysisDate, client, currentFluid]) + return ( + <div className="max-consumption-container"> + <StyledIcon icon={GraphIcon} size={38} /> + <div className="text-16-normal title">{t('analysis.max_day')}</div> + <div className="fluid-navigation"> + {fluidTypes.length > 1 && ( + <IconButton + aria-label={t('consumption.accessibility.button_previous_value')} + onClick={handleChangePrevFluid} + className="arrow-prev" + > + <Icon icon={LeftArrowIcon} size={24} /> + </IconButton> + )} + <div + className={`text-20-bold fluid ${FluidType[ + currentFluid + ].toLowerCase()}`} + > + {`${t('FLUID.' + FluidType[currentFluid] + '.LABEL')}`} + </div> + {fluidTypes.length > 1 && ( + <IconButton + aria-label={t('consumption.accessibility.button_previous_value')} + onClick={handleChangeNextFluid} + className="arrow-next" + > + <Icon icon={RigthArrowIcon} size={24} /> + </IconButton> + )} + </div> + <div className="data-container"> + {maxDayData && !isLoading ? ( + <> + <div className="text-24-bold maxDay-date"> + {maxDayData.date.setLocale('fr').toFormat('cccc dd LLLL')} + </div> + <Icon + className="dataloadvisualizer-euro-fluid-icon" + icon={getNavPicto(currentFluid, true, true)} + size={38} + /> + <div className="maxDay-load"> + {formatNumberValues(maxDayData.value, FluidType[currentFluid]) >= + 1000 ? ( + <> + {formatNumberValues( + maxDayData.value, + FluidType[currentFluid] + )} + + {` ${t('FLUID.' + FluidType[currentFluid] + '.MEGAUNIT')}`} + </> + ) : ( + <> + {formatNumberValues( + maxDayData.value, + FluidType[currentFluid] + )} + + {` ${t('FLUID.' + FluidType[currentFluid] + '.UNIT')}`} + </> + )} + </div> + </> + ) : ( + <StyledSpinner size="3em" fluidType={currentFluid} /> + )} + </div> + </div> + ) +} + +export default MaxConsumptionCard diff --git a/src/components/Analysis/MonthlyAnalysis.tsx b/src/components/Analysis/MonthlyAnalysis.tsx index 7ac167772e7f621187ef4ed809a3ef4dec29031b..1cdea54cbd3db060ca22e7fd0387749e4b132729 100644 --- a/src/components/Analysis/MonthlyAnalysis.tsx +++ b/src/components/Analysis/MonthlyAnalysis.tsx @@ -11,8 +11,6 @@ import { PerformanceIndicator } from 'models' import ConsumptionService from 'services/consumption.service' import PerformanceIndicatorService from 'services/performanceIndicator.service' import ConfigService from 'services/fluidConfig.service' - -import PerformanceIndicatorContent from 'components/PerformanceIndicator/PerformanceIndicatorContent' import FluidPerformanceIndicator from 'components/PerformanceIndicator/FluidPerformanceIndicator' import ProfileEditIcon from 'assets/icons/ico/profile-edit.svg' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' @@ -22,7 +20,7 @@ import { useHistory } from 'react-router-dom' import StyledSpinner from 'components/CommonKit/Spinner/StyledSpinner' import AnalysisErrorModal from './AnalysisErrorModal' import { DateTime } from 'luxon' -import StyledCard from 'components/CommonKit/Card/StyledCard' +import MaxConsumptionCard from './MaxConsumptionCard' import AnalysisIcon from 'assets/icons/visu/analysis/analysis.svg' import TotalAnalysisChart from './TotalAnalysisChart' @@ -139,6 +137,11 @@ const MonthlyAnalysis: React.FC<MonthlyAnalysisProps> = ({ /> </div> </div> + <div className="analysis-content"> + <div className="card"> + <MaxConsumptionCard analysisDate={analysisDate} /> + </div> + </div> <div className="analysis-content"> <div className="card"> <div className="status-header"> diff --git a/src/components/Analysis/__snapshots__/MaxConsumptionCard.spec.tsx.snap b/src/components/Analysis/__snapshots__/MaxConsumptionCard.spec.tsx.snap new file mode 100644 index 0000000000000000000000000000000000000000..ff4e013f8c35fdf709df8140c0f994e51baa984b --- /dev/null +++ b/src/components/Analysis/__snapshots__/MaxConsumptionCard.spec.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MaxConsumptionCard component should be rendered correctly 1`] = ` +<Provider + store={ + Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + } + } +> + <MaxConsumptionCard + analysisDate={"2021-07-01T00:00:00.000Z"} + /> +</Provider> +`; diff --git a/src/components/Analysis/maxConsumptionCard.scss b/src/components/Analysis/maxConsumptionCard.scss new file mode 100644 index 0000000000000000000000000000000000000000..b4592d0239b9c07319436c70b2e66e3977a6d748 --- /dev/null +++ b/src/components/Analysis/maxConsumptionCard.scss @@ -0,0 +1,42 @@ +@import '../../styles/base/color'; + +.max-consumption-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .title { + color: $grey-bright; + margin: 1rem 0 0.7rem 0; + } + .fluid-navigation { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + .fluid { + min-width: 120px; + text-align: center; + } + .electricity { + color: $elec-color; + } + .water { + color: $water-color; + } + .gas { + color: $gas-color; + } + .data-container { + min-height: 130px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + .maxDay-date { + text-transform: capitalize; + margin: 0.8rem; + } + } +} diff --git a/src/locales/fr.json b/src/locales/fr.json index bb368e6bdf735fb671ed1cb0cd3031184e9b9e58..16a5e673e572ce41012b7d9100ea262f3c5831fb 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -81,8 +81,9 @@ "approximative_description": "Pour comparer votre consommation avec un foyer similaire ou avec une conso idéale, veuillez détailler votre profil", "not_connected": "Non connecté", "accessibility": { - "button_go_to_profil": "Détailler mon profil" + "button_go_to_profil": "Aller à la page de profil" }, + "max_day": "Jour où vous avez le plus consommé", "compare": { "title": "Comparateur" } diff --git a/src/services/consumption.service.ts b/src/services/consumption.service.ts index 88172930e53edb2b47acb451c8f68c76a97147e4..4e7fed0fdcbcbef08cf4029d3c5645e11c4bc27c 100644 --- a/src/services/consumption.service.ts +++ b/src/services/consumption.service.ts @@ -112,8 +112,9 @@ export default class ConsumptionDataManager { timeStep: TimeStep, fluidTypes: FluidType[], compareMaxTimePeriod?: TimePeriod, - isHome?: boolean - ): Promise<number | null> { + isHome?: boolean, + withDate?: boolean + ): Promise<number | null | Dataload> { let allData if (isHome) { allData = await this.getGraphData( @@ -133,7 +134,8 @@ export default class ConsumptionDataManager { const max = await this._queryRunnerService.fetchFluidMaxData( maxTimePeriod, timeStep, - fluidTypes[0] + fluidTypes[0], + withDate ) return max } diff --git a/src/services/queryRunner.service.ts b/src/services/queryRunner.service.ts index 50367095a2c0bcab6f5597bfe07f0fa356c748b3..6dfc2a187c1017f48411ed2f52ab15b9da680853 100644 --- a/src/services/queryRunner.service.ts +++ b/src/services/queryRunner.service.ts @@ -340,8 +340,9 @@ export default class QueryRunner { public async fetchFluidMaxData( maxTimePeriod: TimePeriod, timeStep: TimeStep, - fluidType: FluidType - ): Promise<number | null> { + fluidType: FluidType, + withDate?: boolean + ): Promise<number | Dataload | null> { const query: QueryDefinition = this.buildMaxQuery( timeStep, maxTimePeriod, @@ -373,6 +374,9 @@ export default class QueryRunner { if (result && result.data) { const filteredResult = this.filterDataList(result, maxTimePeriod) const mappedResult = this.mapDataList(filteredResult) + if (withDate) { + return mappedResult && mappedResult[0] && mappedResult[0] + } return mappedResult && mappedResult[0] && mappedResult[0].value } return null