From 28ea2817f317792afa71e2be68715435aff69aa6 Mon Sep 17 00:00:00 2001 From: Guilhem CARRON <gcarron@grandlyon.com> Date: Tue, 19 Oct 2021 09:49:19 +0000 Subject: [PATCH] feat(conso): Add new display rules for missing data in the consumption visualizer and in konnector cards BREAKING CHANGE: Removed haveSeenOldFluidDataModal from profile doctype. --- src/assets/icons/ico/questionMark.svg | 3 + .../Connection/ConnectionResult.tsx | 107 ++++++++++-- src/components/Connection/connectionForm.scss | 1 + .../Connection/connectionResult.scss | 10 +- .../DataloadComparisonLeft.spec.tsx | 53 ++++++ .../DataloadComparisonLeft.tsx | 65 +++++++ .../DataloadConsumptionVisualizer.tsx | 122 +++---------- .../DataloadNoValue.spec.tsx | 105 ++++++++++++ .../ConsumptionVisualizer/DataloadNoValue.tsx | 137 +++++++++++++++ .../ErrorDataConsumptionVisualizer.spec.tsx | 73 ++++++++ .../ErrorDataConsumptionVisualizer.tsx | 3 - .../LastDataConsumptionVisualizer.spec.tsx | 64 +++++++ .../LastDataConsumptionVisualizer.tsx | 4 +- .../NoDataModal.spec.tsx | 33 ++++ .../ConsumptionVisualizer/NoDataModal.tsx | 73 ++++++++ .../DataloadComparisonLeft.spec.tsx.snap | 3 + .../DataloadNoValue.spec.tsx.snap | 3 + ...rorDataConsumptionVisualizer.spec.tsx.snap | 3 + ...astDataConsumptionVisualizer.spec.tsx.snap | 3 + .../__snapshots__/NoDataModal.spec.tsx.snap | 3 + .../consumptionVisualizer.scss | 1 + .../dataloadConsumptionVisualizer.scss | 18 +- .../errorDataConsumptionVisualizer.scss | 4 +- .../lastDataConsumptionVisualizer.scss | 5 +- .../ConsumptionVisualizer/noDataModal.scss | 19 +++ src/components/FluidChart/FluidChart.tsx | 64 +------ src/components/FluidChart/FluidChartSlide.tsx | 34 +++- src/components/FluidChart/FluidChartSwipe.tsx | 3 + .../FluidChart/fluidChartSlide.scss | 4 + src/components/Home/ConsumptionView.tsx | 2 + src/components/Home/FluidButton.tsx | 28 ++- .../Home/OldFluidDataModal.spec.tsx | 55 ------ src/components/Home/OldFluidDataModal.tsx | 161 ------------------ .../OldFluidDataModal.spec.tsx.snap | 81 --------- src/components/Home/consumptionDetails.scss | 4 +- src/components/Home/consumptionView.scss | 2 +- src/components/Home/oldFluidDataModal.scss | 54 ------ .../Konnector/KonnectorViewerCard.tsx | 66 ++++--- .../Konnector/konnectorViewerCard.scss | 9 +- src/db/profileData.json | 1 - src/locales/fr.json | 14 +- src/models/profile.model.ts | 7 +- src/services/dateChart.service.spec.ts | 12 +- src/services/dateChart.service.ts | 75 +++++++- src/services/profile.service.spec.ts | 10 +- src/services/profile.service.ts | 28 +-- src/store/profile/profile.reducer.ts | 1 - src/utils/utils.ts | 5 +- tests/__mocks__/profile.mock.ts | 1 - tests/__mocks__/store.ts | 1 - 50 files changed, 994 insertions(+), 643 deletions(-) create mode 100644 src/assets/icons/ico/questionMark.svg create mode 100644 src/components/ConsumptionVisualizer/DataloadComparisonLeft.spec.tsx create mode 100644 src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx create mode 100644 src/components/ConsumptionVisualizer/DataloadNoValue.spec.tsx create mode 100644 src/components/ConsumptionVisualizer/DataloadNoValue.tsx create mode 100644 src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.spec.tsx create mode 100644 src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.spec.tsx create mode 100644 src/components/ConsumptionVisualizer/NoDataModal.spec.tsx create mode 100644 src/components/ConsumptionVisualizer/NoDataModal.tsx create mode 100644 src/components/ConsumptionVisualizer/__snapshots__/DataloadComparisonLeft.spec.tsx.snap create mode 100644 src/components/ConsumptionVisualizer/__snapshots__/DataloadNoValue.spec.tsx.snap create mode 100644 src/components/ConsumptionVisualizer/__snapshots__/ErrorDataConsumptionVisualizer.spec.tsx.snap create mode 100644 src/components/ConsumptionVisualizer/__snapshots__/LastDataConsumptionVisualizer.spec.tsx.snap create mode 100644 src/components/ConsumptionVisualizer/__snapshots__/NoDataModal.spec.tsx.snap create mode 100644 src/components/ConsumptionVisualizer/noDataModal.scss delete mode 100644 src/components/Home/OldFluidDataModal.spec.tsx delete mode 100644 src/components/Home/OldFluidDataModal.tsx delete mode 100644 src/components/Home/__snapshots__/OldFluidDataModal.spec.tsx.snap delete mode 100644 src/components/Home/oldFluidDataModal.scss diff --git a/src/assets/icons/ico/questionMark.svg b/src/assets/icons/ico/questionMark.svg new file mode 100644 index 000000000..b633290c9 --- /dev/null +++ b/src/assets/icons/ico/questionMark.svg @@ -0,0 +1,3 @@ +<svg width="21" height="41" viewBox="0 0 21 41" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 4.09723C0.637168 3.5251 1.31859 2.98987 2.04425 2.49156C2.78761 1.99325 3.58407 1.55953 4.43362 1.19041C5.30088 0.821293 6.23008 0.535225 7.22124 0.33221C8.23008 0.110737 9.30974 0 10.4602 0C12.0531 0 13.4956 0.230701 14.7876 0.692102C16.0797 1.13505 17.1858 1.781 18.1062 2.62998C19.0266 3.4605 19.7345 4.46635 20.2301 5.64754C20.7434 6.82872 21 8.14833 21 9.60635C21 11.009 20.8053 12.2271 20.4159 13.2606C20.0443 14.2757 19.5664 15.1616 18.9823 15.9183C18.4159 16.675 17.7876 17.3302 17.0973 17.8839C16.4071 18.4375 15.7522 18.9543 15.1327 19.4342C14.5133 19.914 13.9734 20.3939 13.5133 20.8737C13.0708 21.3351 12.7965 21.8519 12.6903 22.424L12.0266 26.3829H7.00885L6.50442 21.898C6.38053 21.0122 6.48673 20.2462 6.82301 19.6003C7.15929 18.9359 7.61062 18.336 8.17699 17.8008C8.74337 17.2471 9.37169 16.7211 10.062 16.2228C10.7699 15.7061 11.4248 15.1616 12.0266 14.5895C12.646 13.9989 13.1593 13.3437 13.5664 12.6239C13.9912 11.8857 14.2035 11.0182 14.2035 10.0216C14.2035 8.80351 13.7965 7.83457 12.9823 7.11479C12.1858 6.395 11.1239 6.03511 9.79646 6.03511C8.82301 6.03511 8 6.14585 7.32744 6.36732C6.67257 6.58879 6.10619 6.83795 5.62832 7.11479C5.15044 7.37317 4.74336 7.6131 4.40708 7.83457C4.07079 8.05604 3.75222 8.16678 3.45133 8.16678C2.77876 8.16678 2.27434 7.86226 1.93805 7.25321L0 4.09723ZM4.96461 36.5982C4.96461 35.9892 5.07079 35.4171 5.28318 34.8818C5.51327 34.3282 5.82302 33.8575 6.2124 33.47C6.60178 33.0639 7.05309 32.7502 7.56637 32.5287C8.09734 32.2888 8.67256 32.1688 9.29203 32.1688C9.8938 32.1688 10.4602 32.2888 10.9912 32.5287C11.5221 32.7502 11.9823 33.0639 12.3717 33.47C12.7611 33.8575 13.0619 34.3282 13.2743 34.8818C13.5044 35.4171 13.6195 35.9892 13.6195 36.5982C13.6195 37.2257 13.5044 37.8071 13.2743 38.3423C13.0619 38.8776 12.7611 39.3482 12.3717 39.7542C11.9823 40.1418 11.5221 40.4463 10.9912 40.6678C10.4602 40.8893 9.8938 41 9.29203 41C8.67256 41 8.09734 40.8893 7.56637 40.6678C7.05309 40.4463 6.60178 40.1418 6.2124 39.7542C5.82302 39.3482 5.51327 38.8776 5.28318 38.3423C5.07079 37.8071 4.96461 37.2257 4.96461 36.5982Z" fill="#E3B82A"/> +</svg> diff --git a/src/components/Connection/ConnectionResult.tsx b/src/components/Connection/ConnectionResult.tsx index 518f34863..3e93db6b5 100644 --- a/src/components/Connection/ConnectionResult.tsx +++ b/src/components/Connection/ConnectionResult.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { useClient } from 'cozy-client' import { useDispatch } from 'react-redux' @@ -16,15 +16,20 @@ import StyledBlackSpinner from 'components/CommonKit/Spinner/StyledBlackSpinner' import './connectionResult.scss' import { UsageEventType } from 'enum/usageEvent.enum' import UsageEventService from 'services/usageEvent.service' +import DateChartService from 'services/dateChart.service' +import { FluidType } from 'enum/fluid.enum' +import { DateTime } from 'luxon' interface ConnectionResultProps { fluidStatus: FluidStatus handleAccountDeletion: Function + fluidType: FluidType } const ConnectionResult: React.FC<ConnectionResultProps> = ({ fluidStatus, handleAccountDeletion, + fluidType, }: ConnectionResultProps) => { const { t } = useI18n() const client = useClient() @@ -32,9 +37,12 @@ const ConnectionResult: React.FC<ConnectionResultProps> = ({ const account: Account | null = fluidStatus.connection.account const [updating, setUpdating] = useState<boolean>(false) - const [lastExecutionDate, setLastExecutionDate] = useState<string>('-') + const [lastExecutionDate, setLastExecutionDate] = useState<string | DateTime>( + '-' + ) const [konnectorError, setKonnectorError] = useState<string>('') const [status, setStatus] = useState<string>('') + const [outDatedDataDays, setOutDatedDataDays] = useState<number | null>(null) const updateKonnector = async () => { setUpdating(true) @@ -58,7 +66,6 @@ const ConnectionResult: React.FC<ConnectionResultProps> = ({ : 'error', }) dispatch(updatedFluidConnection(fluidStatus.fluidType, updatedConnection)) - setUpdating(false) } @@ -74,17 +81,37 @@ const ConnectionResult: React.FC<ConnectionResultProps> = ({ setUpdating(false) } } + const isOutdated = useCallback(() => { + const dateChartService = new DateChartService() + + return dateChartService.isDataOutdated(fluidStatus.lastDataDate, fluidType) + }, [fluidStatus, fluidType]) + + const hasUpdatedToday = useCallback(() => { + const todayDate = DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .minus({ days: 1 }) + .toLocaleString() + if (lastExecutionDate === '-') { + return false + } else if (lastExecutionDate > todayDate) { + return false + } else { + return true + } + }, [lastExecutionDate]) useEffect(() => { if ( fluidStatus.connection.triggerState && fluidStatus.connection.triggerState.last_success ) { - setLastExecutionDate( - new Date( - fluidStatus.connection.triggerState.last_success - ).toLocaleString() + const result = DateTime.fromISO( + fluidStatus.connection.triggerState.last_success ) + setLastExecutionDate(result) } else { setLastExecutionDate('-') } @@ -98,12 +125,60 @@ const ConnectionResult: React.FC<ConnectionResultProps> = ({ getKonnectorUpdateError(fluidStatus.connection.triggerState.last_error) ) } - }, [fluidStatus.connection.triggerState]) + + if (isOutdated()) { + setOutDatedDataDays(isOutdated()) + } + }, [fluidStatus.connection.triggerState, isOutdated]) return ( <div className="connection-update-result"> - <div className={status === 'errored' ? 'connection-update-errored' : ''}> - {status === 'errored' && ( + <div + className={ + status === 'errored' && !hasUpdatedToday() + ? 'connection-update-errored' + : '' + } + > + {outDatedDataDays ? ( + <div className="connection-caption text-16-normal"> + <div className="text-16-normal"> + {hasUpdatedToday() === true ? ( + <> + <div className="connection-caption"> + {t('konnector_form.label_updated_at')} + </div> + <div className="text-16-bold"> + {lastExecutionDate.toLocaleString()} + </div> + <div> + {fluidStatus && + fluidStatus.connection && + fluidStatus.connection.konnector && + t('konnector_form.issue') + + ' ' + + fluidStatus.connection.konnector.name + + '.'} + </div> + </> + ) : ( + <div className="connection-caption-errored connection-update-errored warning-white text-16-normal"> + <StyledIcon + icon={warningWhite} + size={36} + className="warning-icon" + role="img" + title="Attention" + ariaHidden={false} + /> + <div className="text-16-normal"> + {t('konnector_form.resolve')} + </div> + </div> + )} + </div> + </div> + ) : status === 'errored' ? ( <div className="connection-caption-errored warning-white text-16-normal"> <StyledIcon icon={warningWhite} @@ -113,21 +188,25 @@ const ConnectionResult: React.FC<ConnectionResultProps> = ({ title="Attention" ariaHidden={false} /> + <div className="text-16-normal"> {t(`konnector_form.${konnectorError}`)} <div className="connection-caption"> {t('konnector_form.label_updated_at')} </div> - <div className="text-16-bold">{lastExecutionDate}</div> + <div className="text-16-bold"> + {lastExecutionDate.toLocaleString()} + </div> </div> </div> - )} - {status !== 'errored' && ( + ) : ( <div> <div className="connection-caption text-16-normal"> {t('konnector_form.label_updated_at')} </div> - <div className="text-16-bold">{lastExecutionDate}</div> + <div className="text-16-bold"> + {lastExecutionDate.toLocaleString()} + </div> </div> )} </div> diff --git a/src/components/Connection/connectionForm.scss b/src/components/Connection/connectionForm.scss index 5e81b0450..15b4af1c6 100644 --- a/src/components/Connection/connectionForm.scss +++ b/src/components/Connection/connectionForm.scss @@ -3,6 +3,7 @@ .konnector-form { margin: 2rem 1.5rem 2rem 1.5rem; + padding-top: 1rem; @media only screen and (min-width : #{$width-large-phone}) { padding-top: 2rem; margin: 4rem 1.5rem 2rem 1.5rem; diff --git a/src/components/Connection/connectionResult.scss b/src/components/Connection/connectionResult.scss index 6c37d89c6..cf5f6477a 100644 --- a/src/components/Connection/connectionResult.scss +++ b/src/components/Connection/connectionResult.scss @@ -10,10 +10,12 @@ .connection-update-errored { background-color: $red-primary; margin: 0 -2.5rem; - padding: 0 2.5rem; + padding: 0.4rem 2.5rem; + display: flex; + align-items: center; @media #{$tablet} { margin: 0 -1.2rem; - padding: 0 1.2rem; + padding: 0.4rem 1.2rem; } .connection-caption-errored { display: flex; @@ -29,10 +31,6 @@ } .connection-caption { color: $grey-bright; - text-transform: lowercase; - &::first-letter { - text-transform: uppercase; - } } } diff --git a/src/components/ConsumptionVisualizer/DataloadComparisonLeft.spec.tsx b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.spec.tsx new file mode 100644 index 000000000..f8823fc77 --- /dev/null +++ b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' + +import configureStore from 'redux-mock-store' +import { mockInitialEcolyoState } from '../../../tests/__mocks__/store' +import { FluidType } from 'enum/fluid.enum' + +import { baseDataLoad } from '../../../tests/__mocks__/datachartData.mock' +import DataloadComparisonLeft from './DataloadComparisonLeft' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockStore = configureStore([]) +describe('DataloadComparisonLeft component', () => { + it('should render correctly', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const wrapper = mount( + <Provider store={store}> + <DataloadComparisonLeft + compareDataload={baseDataLoad} + fluidType={FluidType.ELECTRICITY} + /> + </Provider> + ) + expect(wrapper).toMatchSnapshot() + }) + it('should render empty comparison', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const updatedDataload = { ...baseDataLoad, value: -1 } + const wrapper = mount( + <Provider store={store}> + <DataloadComparisonLeft + compareDataload={updatedDataload} + fluidType={FluidType.ELECTRICITY} + /> + </Provider> + ) + expect(wrapper.find('.dataloadvisualizer-novalue')).toBeTruthy() + }) +}) diff --git a/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx new file mode 100644 index 000000000..69fb32a16 --- /dev/null +++ b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import './consumptionVisualizer.scss' +import { FluidType } from 'enum/fluid.enum' +import { Dataload } from 'models' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import { formatNumberValues } from 'utils/utils' +import ConverterService from 'services/converter.service' + +interface DataloadComparisonLeftProps { + compareDataload: Dataload + fluidType: FluidType +} +const DataloadComparisonLeft: React.FC<DataloadComparisonLeftProps> = ({ + compareDataload, + fluidType, +}: DataloadComparisonLeftProps) => { + const { t } = useI18n() + const converterService = new ConverterService() + + return ( + <> + {compareDataload.value === -1 ? ( + <div className="dataloadvisualizer-section dataloadvisualizer-section-left-novalue"> + <div + className={`dataloadvisualizer-novalue ${FluidType[ + fluidType + ].toLowerCase()}-compare text-20-normal`} + > + {t('consumption_visualizer.missing_data')} + </div> + </div> + ) : ( + <div className="dataloadvisualizer-section dataloadvisualizer-section-left"> + <div + className={`dataloadvisualizer-value ${FluidType[ + fluidType + ].toLowerCase()}-compare text-36-bold`} + > + {formatNumberValues(compareDataload.value)} + <span className="text-18-normal">{`${t( + 'FLUID.' + FluidType[fluidType] + '.UNIT' + )}`}</span> + </div> + <> + {fluidType === FluidType.MULTIFLUID ? ( + <></> + ) : ( + <div + className={`dataloadvisualizer-euro ${FluidType[ + fluidType + ].toLowerCase()}-compare text-16-normal`} + > + {`${formatNumberValues( + converterService.LoadToEuro(compareDataload.value, fluidType) + )} €`} + </div> + )} + </> + </div> + )} + </> + ) +} + +export default DataloadComparisonLeft diff --git a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx index 67396474a..6412b5a3a 100644 --- a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx +++ b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx @@ -18,6 +18,8 @@ import { useClient } from 'cozy-client' import { UsageEventType } from 'enum/usageEvent.enum' import UsageEventService from 'services/usageEvent.service' import EstimatedConsumptionModal from './EstimatedConsumptionModal' +import DataloadComparisonLeft from './DataloadComparisonLeft' +import DataloadNoValue from './DataloadNoValue' interface DataloadConsumptionVisualizerProps { fluidType: FluidType @@ -36,7 +38,9 @@ const DataloadConsumptionVisualizer = ({ setActive, }: DataloadConsumptionVisualizerProps) => { const { t } = useI18n() - const { loading } = useSelector((state: AppStore) => state.ecolyo.chart) + const { loading, currentDatachart } = useSelector( + (state: AppStore) => state.ecolyo.chart + ) const client = useClient() const [openEstimationModal, setOpenEstimationModal] = useState<boolean>(false) const converterService = new ConverterService() @@ -51,79 +55,19 @@ const DataloadConsumptionVisualizer = ({ }, [client] ) - const handleToggleKonnectionCard = useCallback(() => { - setActive(true) - const app = document.querySelector('.app-content') - - const content = document.querySelector('.content-view') - if (content && app) { - setTimeout(() => { - //Desktop devices - app.scrollTo({ - top: content.scrollHeight + 300, - behavior: 'smooth', - }) - //Mobiles devices - app.scrollIntoView({ - behavior: 'smooth', - block: 'end', - inline: 'end', - }) - }, 300) - } - }, [setActive]) const toggleEstimationModal = useCallback(() => { setOpenEstimationModal(prev => !prev) }, []) + return ( <div className="dataloadvisualizer-root"> {!loading && dataload && dataload.value > -1 ? ( <div className="dataloadvisualizer-content"> {showCompare && compareDataload && ( - <> - {compareDataload.value === -1 ? ( - <div className="dataloadvisualizer-section dataloadvisualizer-section-left-novalue"> - <div - className={`dataloadvisualizer-novalue ${FluidType[ - fluidType - ].toLowerCase()}-compare text-20-normal`} - > - {t('consumption_visualizer.no_data')} - </div> - </div> - ) : ( - <div className="dataloadvisualizer-section dataloadvisualizer-section-left"> - <div - className={`dataloadvisualizer-value ${FluidType[ - fluidType - ].toLowerCase()}-compare text-36-bold`} - > - {formatNumberValues(compareDataload.value)} - <span className="text-18-normal">{`${t( - 'FLUID.' + FluidType[fluidType] + '.UNIT' - )}`}</span> - </div> - <> - {fluidType === FluidType.MULTIFLUID ? ( - <></> - ) : ( - <div - className={`dataloadvisualizer-euro ${FluidType[ - fluidType - ].toLowerCase()}-compare text-16-normal`} - > - {`${formatNumberValues( - converterService.LoadToEuro( - compareDataload.value, - fluidType - ) - )} €`} - </div> - )} - </> - </div> - )} - </> + <DataloadComparisonLeft + fluidType={fluidType} + compareDataload={compareDataload} + /> )} <div className={ @@ -157,7 +101,7 @@ const DataloadConsumptionVisualizer = ({ > {`${t('FLUID.' + FluidType[fluidType] + '.UNIT')}`} </span> - {fluidType === FluidType.MULTIFLUID && !showCompare && ( + {fluidType === FluidType.MULTIFLUID && ( <span className="text-14-normal estimated" onClick={toggleEstimationModal} @@ -213,17 +157,19 @@ const DataloadConsumptionVisualizer = ({ size={22} /> <div> - {!dateChartService.isDataToCome( - dataload, - index - ) && load !== -1 + {load !== -1 ? `${formatNumberValues(load)} €` - : dateChartService.isDataToCome( + : !dateChartService.isDataToCome( dataload, index ) - ? t('consumption_visualizer.data_to_come') - : '---- €'} + ? dateChartService.isDataHole( + currentDatachart, + index + ) + ? '---- €' + : t('consumption_visualizer.aie') + : t('consumption_visualizer.data_to_come')} </div> </div> </NavLink> @@ -262,28 +208,12 @@ const DataloadConsumptionVisualizer = ({ </div> </div> ) : ( - <> - {!dataload ? null : dataload && - lastDataDate && - dataload.date > lastDataDate ? ( - <div - className={`dataloadvisualizer-content ${FluidType[ - fluidType - ].toLowerCase()} text-22-normal`} - > - {`${t( - 'consumption_visualizer.last_data' - )} : ${lastDataDate.toFormat("dd'/'MM'/'yy")}`} - </div> - ) : ( - <div - onClick={handleToggleKonnectionCard} - className={`dataloadvisualizer-content text-22-normal error`} - > - {`${t('consumption_visualizer.no_data')}`} - </div> - )} - </> + <DataloadNoValue + lastDataDate={lastDataDate} + dataload={dataload} + setActive={setActive} + fluidType={fluidType} + /> )} <EstimatedConsumptionModal open={openEstimationModal} diff --git a/src/components/ConsumptionVisualizer/DataloadNoValue.spec.tsx b/src/components/ConsumptionVisualizer/DataloadNoValue.spec.tsx new file mode 100644 index 000000000..3a2cf00da --- /dev/null +++ b/src/components/ConsumptionVisualizer/DataloadNoValue.spec.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' +import * as reactRedux from 'react-redux' +import configureStore from 'redux-mock-store' +import { mockInitialEcolyoState } from '../../../tests/__mocks__/store' +import { FluidType } from 'enum/fluid.enum' +import DataloadNoValue from './DataloadNoValue' +import { + baseDataLoad, + graphData, +} from '../../../tests/__mocks__/datachartData.mock' +import { DateTime } from 'luxon' +import { Datachart } from 'models' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockStore = configureStore([]) +describe('DataloadNoValue component', () => { + it('should render correctly', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockSetActive = jest.fn() + const wrapper = mount( + <Provider store={store}> + <DataloadNoValue + dataload={baseDataLoad} + fluidType={FluidType.ELECTRICITY} + lastDataDate={null} + setActive={mockSetActive} + /> + </Provider> + ) + expect(wrapper).toMatchSnapshot() + }) + it('should render with missing data state and click on the error button', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockSetActive = jest.fn() + const wrapper = mount( + <Provider store={store}> + <DataloadNoValue + dataload={baseDataLoad} + fluidType={FluidType.ELECTRICITY} + lastDataDate={null} + setActive={mockSetActive} + /> + </Provider> + ) + expect(wrapper.find('.error').simulate('click')) + }) + + it('should render with data-to-come state', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const updatedDataLoad = { ...baseDataLoad, date: DateTime.local() } + const mockSetActive = jest.fn() + const wrapper = mount( + <Provider store={store}> + <DataloadNoValue + dataload={updatedDataLoad} + fluidType={FluidType.ELECTRICITY} + lastDataDate={null} + setActive={mockSetActive} + /> + </Provider> + ) + expect(wrapper.find('.to-come')).toBeTruthy() + }) + it('should render with data-hole state', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockSetActive = jest.fn() + const mockUseSelector = jest.spyOn(reactRedux, 'useSelector') + const updatedData: Datachart = { ...graphData } + //Data hole insertion + updatedData.actualData[0].value = -1 + updatedData.actualData[1].value = 90 + + mockUseSelector.mockReturnValue({ currentDatachart: graphData }) + const wrapper = mount( + <Provider store={store}> + <DataloadNoValue + dataload={baseDataLoad} + fluidType={FluidType.ELECTRICITY} + lastDataDate={DateTime.local()} + setActive={mockSetActive} + /> + </Provider> + ) + expect(wrapper.find('.no-data-text').simulate('click')) + }) +}) diff --git a/src/components/ConsumptionVisualizer/DataloadNoValue.tsx b/src/components/ConsumptionVisualizer/DataloadNoValue.tsx new file mode 100644 index 000000000..8ae100b80 --- /dev/null +++ b/src/components/ConsumptionVisualizer/DataloadNoValue.tsx @@ -0,0 +1,137 @@ +import React, { useCallback, useState } from 'react' +import './consumptionVisualizer.scss' +import { FluidType } from 'enum/fluid.enum' +import { Dataload } from 'models' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' + +import { DateTime } from 'luxon' +import { useSelector } from 'react-redux' +import { AppStore } from 'store' +import ConfigService from 'services/fluidConfig.service' +import NoDataModal from './NoDataModal' +import DateChartService from 'services/dateChart.service' + +interface DataloadNoValueProps { + dataload: Dataload + fluidType: FluidType + lastDataDate: DateTime | null + setActive: React.Dispatch<React.SetStateAction<boolean>> +} +enum NoDataState { + DATA_TO_COME = 0, + DATA_HOLE = 1, + MISSING_DATA = 2, +} + +const DataloadNoValue: React.FC<DataloadNoValueProps> = ({ + dataload, + fluidType, + setActive, + lastDataDate, +}: DataloadNoValueProps) => { + const { t } = useI18n() + const { currentDatachart } = useSelector( + (state: AppStore) => state.ecolyo.chart + ) + const configService = new ConfigService() + const fluidConfig = configService.getFluidConfig() + const [openNodataModal, setopenNodataModal] = useState<boolean>(false) + const toggleNoDataModal = useCallback(() => { + setopenNodataModal(prev => !prev) + }, []) + const handleToggleKonnectionCard = useCallback(() => { + setActive(true) + const app = document.querySelector('.app-content') + const content = document.querySelector('.content-view') + if (content && app) { + setTimeout(() => { + //Desktop devices + app.scrollTo({ + top: content.scrollHeight + 300, + behavior: 'smooth', + }) + //Mobiles devices + app.scrollIntoView({ + behavior: 'smooth', + block: 'end', + inline: 'end', + }) + }, 300) + } + }, [setActive]) + + const getDataState = useCallback(() => { + if (fluidType !== FluidType.MULTIFLUID) { + //J+3 for elec and J+5 for the other ==> dataDelayPffset + 3 + const delay = fluidConfig[fluidType].dataDelayOffset + 3 + const today = DateTime.local().setZone('utc', { + keepLocalTime: true, + }) + const offsetDate = today.minus({ days: delay }) + if (dataload && offsetDate < dataload.date) { + return NoDataState.DATA_TO_COME + } else if (dataload && dataload.date < offsetDate) { + const dateChartService = new DateChartService() + if ( + dateChartService.isDataHole(currentDatachart) && + lastDataDate && + lastDataDate > dataload.date + ) { + return NoDataState.DATA_HOLE + } else return NoDataState.MISSING_DATA + } else return NoDataState.MISSING_DATA + } + return NoDataState.DATA_TO_COME + }, [currentDatachart, dataload, fluidConfig, fluidType, lastDataDate]) + + return ( + <> + {getDataState() === NoDataState.DATA_TO_COME && ( + <div className={`dataloadvisualizer-content text-22-normal`}> + <div className="dataloadvisualizer-section"> + <div + className={`dataloadvisualizer-value ${FluidType[ + fluidType + ].toLowerCase()} upper to-come`} + > + {t('consumption_visualizer.data_to_come')} + </div> + </div> + </div> + )} + {getDataState() === NoDataState.MISSING_DATA && ( + <div + onClick={handleToggleKonnectionCard} + className={`dataloadvisualizer-content error text-22-normal`} + > + {t('consumption_visualizer.missing_data')} + </div> + )} + {getDataState() === NoDataState.DATA_HOLE && ( + <div className={`dataloadvisualizer-content text-22-normal`}> + <div className="dataloadvisualizer-section"> + <div + className={`dataloadvisualizer-value ${FluidType[ + fluidType + ].toLowerCase()} upper`} + > + {t('consumption_visualizer.no_data')} + </div> + <div + className="text-15-normal no-data-text" + onClick={toggleNoDataModal} + > + {t('consumption_visualizer.why_no_data')} + </div> + </div> + </div> + )} + <NoDataModal + open={openNodataModal} + handleCloseClick={toggleNoDataModal} + /> + </> + ) +} + +export default DataloadNoValue diff --git a/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.spec.tsx b/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.spec.tsx new file mode 100644 index 000000000..2f56ebdb1 --- /dev/null +++ b/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.spec.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' +import * as reactRedux from 'react-redux' + +import configureStore from 'redux-mock-store' +import { mockInitialEcolyoState } from '../../../tests/__mocks__/store' +import ErrorDataConsumptionVisualizer from './ErrorDataConsumptionVisualizer' +import { FluidType } from 'enum/fluid.enum' +import { FluidStatus } from 'models' +import { fluidStatusData } from '../../../tests/__mocks__/fluidStatusData.mock' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockStore = configureStore([]) +describe('ErrorDataConsumptionVisualizer component', () => { + it('should render correctly', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const wrapper = mount( + <Provider store={store}> + <ErrorDataConsumptionVisualizer fluidType={FluidType.ELECTRICITY} /> + </Provider> + ) + expect(wrapper).toMatchSnapshot() + }) + it('should click and move to lastDataDate', async () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockUseDispatch = jest.spyOn(reactRedux, 'useDispatch') + + const wrapper = mount( + <Provider store={store}> + <ErrorDataConsumptionVisualizer fluidType={FluidType.MULTIFLUID} /> + </Provider> + ) + wrapper.find('.error-line').simulate('click') + expect(mockUseDispatch).toHaveBeenCalled() + }) + it('should render with Electricity and no LastDataDate', async () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockUseDispatch = jest.spyOn(reactRedux, 'useDispatch') + const mockUseSelector = jest.spyOn(reactRedux, 'useSelector') + const updatedFluidStatus: FluidStatus[] = { ...fluidStatusData } + updatedFluidStatus[0].lastDataDate = null + mockUseSelector.mockReturnValueOnce({ + fluidStatus: updatedFluidStatus, + fluidTypes: [FluidType.ELECTRICITY], + }) + const wrapper = mount( + <Provider store={store}> + <ErrorDataConsumptionVisualizer fluidType={FluidType.ELECTRICITY} /> + </Provider> + ) + wrapper.find('.error-line').simulate('click') + expect(mockUseDispatch).toHaveBeenCalled() + expect(wrapper.find('.underlined-error').text()).toBe( + 'consumption_visualizer.last_valid_data : -' + ) + }) +}) diff --git a/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.tsx b/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.tsx index e2f778065..7f4c4c3c4 100644 --- a/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.tsx +++ b/src/components/ConsumptionVisualizer/ErrorDataConsumptionVisualizer.tsx @@ -6,8 +6,6 @@ import { AppStore } from 'store' import { setCurrentIndex, setSelectedDate } from 'store/chart/chart.actions' import { DateTime } from 'luxon' import DateChartService from 'services/dateChart.service' -import warning from 'assets/icons/ico/warning.svg' -import StyledIcon from 'components/CommonKit/Icon/StyledIcon' import { FluidType } from 'enum/fluid.enum' interface ErrorDataConsumptionVisualizerProps { @@ -61,7 +59,6 @@ const ErrorDataConsumptionVisualizer: React.FC<ErrorDataConsumptionVisualizerPro return ( <div onClick={() => setDateAndMoveToindex()} className="error-line"> - <StyledIcon icon={warning} size={22} className="warning-icon" /> <span className={`text-16-normal underlined-error`}> {`${t('consumption_visualizer.last_valid_data')} : ${ lastDateWithAllData diff --git a/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.spec.tsx b/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.spec.tsx new file mode 100644 index 000000000..b299ed7ac --- /dev/null +++ b/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.spec.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' +import * as reactRedux from 'react-redux' + +import configureStore from 'redux-mock-store' +import { mockInitialEcolyoState } from '../../../tests/__mocks__/store' +import LastDataConsumptionVisualizer from './LastDataConsumptionVisualizer' +import { DateTime } from 'luxon' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockStore = configureStore([]) +describe('LastDataConsumptionVisualizer component', () => { + it('should render correctly', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const wrapper = mount( + <Provider store={store}> + <LastDataConsumptionVisualizer lastDataDate={null} /> + </Provider> + ) + expect(wrapper).toMatchSnapshot() + }) + it('should click and move to last data date', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const mockUseDispatch = jest.spyOn(reactRedux, 'useDispatch') + + const wrapper = mount( + <Provider store={store}> + <LastDataConsumptionVisualizer lastDataDate={DateTime.local()} /> + </Provider> + ) + wrapper.find('.lastdatavisualizer-button').simulate('click') + expect(mockUseDispatch).toHaveBeenCalled() + }) + it('should render empty last data date', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + + const wrapper = mount( + <Provider store={store}> + <LastDataConsumptionVisualizer lastDataDate={null} /> + </Provider> + ) + wrapper.find('.lastdatavisualizer-button').simulate('click') + + expect(wrapper.find('.lastdatavisualizer-button').text()).toBe( + 'consumption_visualizer.last_valid_data : -' + ) + }) +}) diff --git a/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.tsx b/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.tsx index 6d2a32803..d395f63d9 100644 --- a/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.tsx +++ b/src/components/ConsumptionVisualizer/LastDataConsumptionVisualizer.tsx @@ -35,7 +35,9 @@ const LastDataConsumptionVisualizer: React.FC<LastDataConsumptionVisualizerProps return ( <div> <button className="lastdatavisualizer-button" onClick={moveToDate}> - {t('consumption.display_last_data')} + {`${t('consumption_visualizer.last_valid_data')} : ${ + lastDataDate ? lastDataDate.toFormat("dd'/'MM'/'yy") : '-' + }`} </button> </div> ) diff --git a/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx b/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx new file mode 100644 index 000000000..d233665e1 --- /dev/null +++ b/src/components/ConsumptionVisualizer/NoDataModal.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { mount } from 'enzyme' +import { Provider } from 'react-redux' + +import configureStore from 'redux-mock-store' +import { mockInitialEcolyoState } from '../../../tests/__mocks__/store' +import NoDataModal from './NoDataModal' + +jest.mock('cozy-ui/transpiled/react/I18n', () => { + return { + useI18n: jest.fn(() => { + return { + t: (str: string) => str, + } + }), + } +}) + +const mockStore = configureStore([]) +const mockHandleClose = jest.fn() +describe('NoDataModal component', () => { + it('should render correctly', () => { + const store = mockStore({ + ecolyo: mockInitialEcolyoState, + }) + const wrapper = mount( + <Provider store={store}> + <NoDataModal open={true} handleCloseClick={mockHandleClose} /> + </Provider> + ) + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/src/components/ConsumptionVisualizer/NoDataModal.tsx b/src/components/ConsumptionVisualizer/NoDataModal.tsx new file mode 100644 index 000000000..f6c64f7a1 --- /dev/null +++ b/src/components/ConsumptionVisualizer/NoDataModal.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { useI18n } from 'cozy-ui/transpiled/react/I18n' +import Dialog from '@material-ui/core/Dialog' +import CloseIcon from 'assets/icons/ico/close.svg' +import QuestionIcon from 'assets/icons/ico/questionMark.svg' +import Icon from 'cozy-ui/transpiled/react/Icon' +import { Button, IconButton } from '@material-ui/core' +import './noDataModal.scss' + +interface NoDataModalProps { + open: boolean + handleCloseClick: () => void +} + +const NoDataModal: React.FC<NoDataModalProps> = ({ + open, + handleCloseClick, +}: NoDataModalProps) => { + const { t } = useI18n() + + return ( + <Dialog + open={open} + onClose={handleCloseClick} + aria-labelledby={'accessibility-title'} + classes={{ + root: 'modal-root', + paper: 'modal-paper', + }} + > + <div id={'accessibility-title'}> + {t('consumption_visualizer.modal.window_title')} + </div> + <IconButton + aria-label={t('consumption_visualizer.modal.close')} + className="modal-paper-close-button" + onClick={handleCloseClick} + > + <Icon icon={CloseIcon} size={16} /> + </IconButton> + <div className="nodata-modal"> + <div className="question-mark"> + <Icon icon={QuestionIcon} size={36} /> + </div> + + <div className="text-20-normal title"> + {t('consumption_visualizer.why_no_data')} + </div> + <div className="text-16-normal"> + {t('consumption_visualizer.dataModal.list_title')} + </div> + <div className="text-16-normal"> + {t('consumption_visualizer.dataModal.item1')} + </div> + <div className="text-16-normal"> + {t('consumption_visualizer.dataModal.item2')} + </div> + <Button + aria-label={t('ecogesture_info_modal.button_close')} + onClick={handleCloseClick} + classes={{ + root: 'btn-highlight', + label: 'text-16-bold', + }} + > + {t('ecogesture_info_modal.button_close')} + </Button> + </div> + </Dialog> + ) +} + +export default NoDataModal diff --git a/src/components/ConsumptionVisualizer/__snapshots__/DataloadComparisonLeft.spec.tsx.snap b/src/components/ConsumptionVisualizer/__snapshots__/DataloadComparisonLeft.spec.tsx.snap new file mode 100644 index 000000000..9f5722378 --- /dev/null +++ b/src/components/ConsumptionVisualizer/__snapshots__/DataloadComparisonLeft.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataloadComparisonLeft component should render correctly 1`] = `ReactWrapper {}`; diff --git a/src/components/ConsumptionVisualizer/__snapshots__/DataloadNoValue.spec.tsx.snap b/src/components/ConsumptionVisualizer/__snapshots__/DataloadNoValue.spec.tsx.snap new file mode 100644 index 000000000..372c0275b --- /dev/null +++ b/src/components/ConsumptionVisualizer/__snapshots__/DataloadNoValue.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataloadNoValue component should render correctly 1`] = `ReactWrapper {}`; diff --git a/src/components/ConsumptionVisualizer/__snapshots__/ErrorDataConsumptionVisualizer.spec.tsx.snap b/src/components/ConsumptionVisualizer/__snapshots__/ErrorDataConsumptionVisualizer.spec.tsx.snap new file mode 100644 index 000000000..a95a17a50 --- /dev/null +++ b/src/components/ConsumptionVisualizer/__snapshots__/ErrorDataConsumptionVisualizer.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorDataConsumptionVisualizer component should render correctly 1`] = `ReactWrapper {}`; diff --git a/src/components/ConsumptionVisualizer/__snapshots__/LastDataConsumptionVisualizer.spec.tsx.snap b/src/components/ConsumptionVisualizer/__snapshots__/LastDataConsumptionVisualizer.spec.tsx.snap new file mode 100644 index 000000000..c10fc2c61 --- /dev/null +++ b/src/components/ConsumptionVisualizer/__snapshots__/LastDataConsumptionVisualizer.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LastDataConsumptionVisualizer component should render correctly 1`] = `ReactWrapper {}`; diff --git a/src/components/ConsumptionVisualizer/__snapshots__/NoDataModal.spec.tsx.snap b/src/components/ConsumptionVisualizer/__snapshots__/NoDataModal.spec.tsx.snap new file mode 100644 index 000000000..f7551fdfb --- /dev/null +++ b/src/components/ConsumptionVisualizer/__snapshots__/NoDataModal.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoDataModal component should render correctly 1`] = `ReactWrapper {}`; diff --git a/src/components/ConsumptionVisualizer/consumptionVisualizer.scss b/src/components/ConsumptionVisualizer/consumptionVisualizer.scss index 4b5fc3f8a..d9b772b79 100644 --- a/src/components/ConsumptionVisualizer/consumptionVisualizer.scss +++ b/src/components/ConsumptionVisualizer/consumptionVisualizer.scss @@ -5,6 +5,7 @@ display: flex; flex-direction: column; align-items: center; + padding-bottom: 1rem; @media #{$large-phone} { width: 100%; } diff --git a/src/components/ConsumptionVisualizer/dataloadConsumptionVisualizer.scss b/src/components/ConsumptionVisualizer/dataloadConsumptionVisualizer.scss index c5659df8e..d7fe9d5b8 100644 --- a/src/components/ConsumptionVisualizer/dataloadConsumptionVisualizer.scss +++ b/src/components/ConsumptionVisualizer/dataloadConsumptionVisualizer.scss @@ -2,7 +2,7 @@ @import 'src/styles/base/breakpoint'; .dataloadvisualizer-root { - min-height: 5.25rem; + min-height: 5rem; display: flex; align-items: center; } @@ -18,14 +18,22 @@ font-weight: bold; text-decoration: underline; } + .dataloadvisualizer-section { display: flex; flex-direction: column; align-items: center; + .no-data-text { + cursor: pointer; + color: $grey-bright; + border-bottom: solid 1px $grey-bright; + margin-top: 1rem; + } &.dataloadvisualizer-section-left { align-items: flex-end; padding: 0.5rem 0.5rem; } + &.dataloadvisualizer-section-left-novalue { align-items: flex-start; padding: 0.5rem 0.5rem 0.5rem 0; @@ -54,6 +62,12 @@ top: -12px; } } + .upper { + font-weight: bold; + &:first-letter { + text-transform: uppercase; + } + } .electricity { color: $elec-color; } @@ -86,6 +100,8 @@ .dataloadvisualizer-euro { display: flex; flex-direction: row; + min-height: 2rem; + align-items: center; .dataloadvisualizer-euro-link { text-decoration: none; color: transparent; diff --git a/src/components/ConsumptionVisualizer/errorDataConsumptionVisualizer.scss b/src/components/ConsumptionVisualizer/errorDataConsumptionVisualizer.scss index f53c69afa..9482a48fb 100644 --- a/src/components/ConsumptionVisualizer/errorDataConsumptionVisualizer.scss +++ b/src/components/ConsumptionVisualizer/errorDataConsumptionVisualizer.scss @@ -1,13 +1,13 @@ @import 'src/styles/base/color'; .error-line { - color: $red-primary; + color: $grey-bright; cursor: pointer; display: flex; align-items: center; } .underlined-error { - border-bottom: solid 1px $red-primary; + border-bottom: solid 1px $grey-bright; } .warning-icon { margin-right: 4px; diff --git a/src/components/ConsumptionVisualizer/lastDataConsumptionVisualizer.scss b/src/components/ConsumptionVisualizer/lastDataConsumptionVisualizer.scss index 5a85afc64..381b55bba 100644 --- a/src/components/ConsumptionVisualizer/lastDataConsumptionVisualizer.scss +++ b/src/components/ConsumptionVisualizer/lastDataConsumptionVisualizer.scss @@ -1,8 +1,9 @@ @import 'src/styles/base/color'; .lastdatavisualizer-button { + cursor: pointer; border: none; background: none; - color: $soft-grey; + color: $grey-bright; text-decoration: underline; -} \ No newline at end of file +} diff --git a/src/components/ConsumptionVisualizer/noDataModal.scss b/src/components/ConsumptionVisualizer/noDataModal.scss new file mode 100644 index 000000000..f2e825050 --- /dev/null +++ b/src/components/ConsumptionVisualizer/noDataModal.scss @@ -0,0 +1,19 @@ +@import 'src/styles/base/color'; + +.nodata-modal { + padding: 0.5rem; + color: $grey-bright; + div { + margin: 0.5rem 0; + line-height: 1.3; + } + .question-mark { + text-align: center; + } + .title { + color: $gold-shadow; + font-weight: bold; + text-align: center; + margin: 1rem 0; + } +} diff --git a/src/components/FluidChart/FluidChart.tsx b/src/components/FluidChart/FluidChart.tsx index 3375fad3a..d25aa6efa 100644 --- a/src/components/FluidChart/FluidChart.tsx +++ b/src/components/FluidChart/FluidChart.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect } from 'react' import './fluidChart.scss' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { useClient } from 'cozy-client' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { AppStore } from 'store' import useExploration from 'components/Hooks/useExploration' @@ -15,12 +15,6 @@ import StyledSwitch from 'components/CommonKit/Switch/StyledSwitch' import TimeStepSelector from 'components/TimeStepSelector/TimeStepSelector' import ActivateHalfHourLoad from 'components/ActivateHalfHourLoad/ActivateHalfHourLoad' import FluidChartSwipe from './FluidChartSwipe' -import ConsumptionVisualizer from 'components/ConsumptionVisualizer/ConsumptionVisualizer' -import { Profile } from 'models' -import { DateTime } from 'luxon' -import { updateProfile } from 'store/profile/profile.actions' -import OldFluidDataModal from 'components/Home/OldFluidDataModal' -import FluidService from 'services/fluid.service' import { UsageEventType } from 'enum/usageEvent.enum' import UsageEventService from 'services/usageEvent.service' @@ -35,11 +29,7 @@ const FluidChart: React.FC<FluidChartProps> = ({ }: FluidChartProps) => { const { t } = useI18n() const client = useClient() - const dispatch = useDispatch() const { fluidStatus } = useSelector((state: AppStore) => state.ecolyo.global) - const profile: Profile = useSelector( - (state: AppStore) => state.ecolyo.profile - ) const { currentTimeStep } = useSelector( (state: AppStore) => state.ecolyo.chart ) @@ -47,19 +37,6 @@ const FluidChart: React.FC<FluidChartProps> = ({ const [isMinuteBlocked, setMinuteBlocked] = useState<boolean>(false) const [isLoaded, setIsLoaded] = useState<boolean>(false) const [showCompare, setShowCompare] = useState<boolean>(false) - const [openOldFluidDataModal, setopenOldFluidDataModal] = useState(false) - const [fluidOldData, setFluidOldData] = useState<FluidType[]>([]) - - const handleCloseClick = useCallback(() => { - dispatch( - updateProfile({ - haveSeenOldFluidModal: DateTime.local().setZone('utc', { - keepLocalTime: true, - }), - }) - ) - setopenOldFluidDataModal(false) - }, [dispatch]) const handleChangeSwitch = async () => { if (!showCompare) { @@ -85,29 +62,13 @@ const FluidChart: React.FC<FluidChartProps> = ({ setMinuteBlocked(true) } setIsLoaded(true) - const oldFluidData = await FluidService.getOldFluidData(fluidStatus) - const lastSeen: boolean | DateTime = profile.haveSeenOldFluidModal - if (subscribed && oldFluidData.length > 0) { - setFluidOldData(oldFluidData) - if (lastSeen === false && subscribed) { - setopenOldFluidDataModal(true) - } else if (lastSeen) { - const diff = - typeof lastSeen === 'boolean' - ? 0 - : lastSeen.diffNow('hours').toObject().hours - if (diff && diff < -23) { - setopenOldFluidDataModal(true) - } - } - } } } loadData() return () => { subscribed = false } - }, [client, fluidStatus, profile.haveSeenOldFluidModal]) + }, [client, fluidStatus]) useEffect(() => { if (!isMinuteBlocked && currentTimeStep === TimeStep.HALF_AN_HOUR) { @@ -126,18 +87,12 @@ const FluidChart: React.FC<FluidChartProps> = ({ <ActivateHalfHourLoad /> ) : ( <div className="fluidchart-content"> - <ConsumptionVisualizer - fluidType={fluidType} - showCompare={ - currentTimeStep === TimeStep.YEAR ? false : showCompare - } - setActive={setActive} - /> <FluidChartSwipe fluidType={fluidType} showCompare={ currentTimeStep === TimeStep.YEAR ? false : showCompare } + setActive={setActive} /> </div> )} @@ -171,17 +126,6 @@ const FluidChart: React.FC<FluidChartProps> = ({ </div> ) : null} </div> - {/* - TODO to be removed in US 562 - */} - {/* {fluidStatus.length > 0 && ( - <OldFluidDataModal - open={openOldFluidDataModal} - fluidStatus={fluidStatus} - fluidOldData={fluidOldData} - handleCloseClick={handleCloseClick} - /> - )} */} </> )} </> diff --git a/src/components/FluidChart/FluidChartSlide.tsx b/src/components/FluidChart/FluidChartSlide.tsx index b3be15076..c459e9c77 100644 --- a/src/components/FluidChart/FluidChartSlide.tsx +++ b/src/components/FluidChart/FluidChartSlide.tsx @@ -14,6 +14,7 @@ import DateChartService from 'services/dateChart.service' import BarChart from 'components/Charts/BarChart' import StyledSpinner from 'components/CommonKit/Spinner/StyledSpinner' import { TimeStep } from 'enum/timeStep.enum' +import ConsumptionVisualizer from 'components/ConsumptionVisualizer/ConsumptionVisualizer' interface FluidChartSlideProps { index: number @@ -22,6 +23,7 @@ interface FluidChartSlideProps { width: number height: number isSwitching: boolean + setActive: React.Dispatch<React.SetStateAction<boolean>> } const FluidChartSlide: React.FC<FluidChartSlideProps> = ({ @@ -31,6 +33,7 @@ const FluidChartSlide: React.FC<FluidChartSlideProps> = ({ width, height, isSwitching, + setActive, }: FluidChartSlideProps) => { const client = useClient() const dispatch = useDispatch() @@ -115,17 +118,30 @@ const FluidChartSlide: React.FC<FluidChartSlideProps> = ({ return ( <div className={'fluidchartslide-root'} aria-busy={!isDataLoaded}> {!isDataLoaded ? ( - <StyledSpinner size="5em" fluidType={fluidType} /> - ) : ( - <BarChart - chartData={chartData} + <StyledSpinner + size="5em" fluidType={fluidType} - timeStep={timeStep} - showCompare={showCompare} - height={height} - width={width} - isSwitching={isSwitching} + className="data-spinner" /> + ) : ( + <> + <ConsumptionVisualizer + fluidType={fluidType} + showCompare={ + currentTimeStep === TimeStep.YEAR ? false : showCompare + } + setActive={setActive} + /> + <BarChart + chartData={chartData} + fluidType={fluidType} + timeStep={timeStep} + showCompare={showCompare} + height={height} + width={width} + isSwitching={isSwitching} + /> + </> )} </div> ) diff --git a/src/components/FluidChart/FluidChartSwipe.tsx b/src/components/FluidChart/FluidChartSwipe.tsx index 777b8bd51..246291866 100644 --- a/src/components/FluidChart/FluidChartSwipe.tsx +++ b/src/components/FluidChart/FluidChartSwipe.tsx @@ -16,11 +16,13 @@ const VirtualizeSwipeableViews = virtualize(SwipeableViews) interface FluidChartSwipeProps { fluidType: FluidType showCompare: boolean + setActive: React.Dispatch<React.SetStateAction<boolean>> } const FluidChartSwipe: React.FC<FluidChartSwipeProps> = ({ fluidType, showCompare, + setActive, }: FluidChartSwipeProps) => { const dispatch = useDispatch() const { currentIndex, currentTimeStep, selectedDate, loading } = useSelector( @@ -112,6 +114,7 @@ const FluidChartSwipe: React.FC<FluidChartSwipeProps> = ({ width={width} height={height} isSwitching={isSwitching} + setActive={setActive} /> )} enableMouseEvents diff --git a/src/components/FluidChart/fluidChartSlide.scss b/src/components/FluidChart/fluidChartSlide.scss index e3dfd5017..cd629bab5 100644 --- a/src/components/FluidChart/fluidChartSlide.scss +++ b/src/components/FluidChart/fluidChartSlide.scss @@ -4,9 +4,13 @@ min-height: 22rem; overflow-x: hidden; display: flex; + flex-direction: column; justify-content: center; align-items: center; @media #{$large-phone} { min-height: 14rem; } + .data-spinner { + margin-top: 4rem; + } } diff --git a/src/components/Home/ConsumptionView.tsx b/src/components/Home/ConsumptionView.tsx index c50430cc9..8051b2cea 100644 --- a/src/components/Home/ConsumptionView.tsx +++ b/src/components/Home/ConsumptionView.tsx @@ -81,6 +81,7 @@ const ConsumptionView: React.FC<ConsumptionViewProps> = ({ <div className="konnector-section"> <KonnectorViewerCard fluidStatus={fluidStatus[fluidType]} + fluidType={fluidType} isParam={true} isDisconnected={false} setActive={setActive} @@ -93,6 +94,7 @@ const ConsumptionView: React.FC<ConsumptionViewProps> = ({ <div className="konnector-section"> <KonnectorViewerCard fluidStatus={fluidStatus[fluidType]} + fluidType={fluidType} isParam={false} isDisconnected={true} setActive={setActive} diff --git a/src/components/Home/FluidButton.tsx b/src/components/Home/FluidButton.tsx index a3b380e49..7be422f8b 100644 --- a/src/components/Home/FluidButton.tsx +++ b/src/components/Home/FluidButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { FluidState, FluidType } from 'enum/fluid.enum' @@ -12,6 +12,7 @@ import { useSelector } from 'react-redux' import { AppStore } from 'store' import { isKonnectorActive } from 'utils/utils' import ErrorNotif from 'assets/icons/ico/notif_error.svg' +import DateChartService from 'services/dateChart.service' interface FluidButtonProps { fluidType: FluidType @@ -26,6 +27,7 @@ const FluidButton: React.FC<FluidButtonProps> = ({ const history = useHistory() const { fluidStatus } = useSelector((state: AppStore) => state.ecolyo.global) const client = useClient() + const [showError, setShowError] = useState<boolean>(false) const isConnected = useCallback(() => { if (fluidType === FluidType.MULTIFLUID) { @@ -43,6 +45,15 @@ const FluidButton: React.FC<FluidButtonProps> = ({ return false }, [fluidStatus, fluidType]) + const isOutdated = useCallback(() => { + const dateChartService = new DateChartService() + + return dateChartService.isDataOutdated( + fluidStatus[fluidType].lastDataDate, + fluidType + ) + }, [fluidStatus, fluidType]) + const iconType = getNavPicto(fluidType, isActive, isConnected()) const goToFluid = useCallback(async () => { @@ -56,6 +67,19 @@ const FluidButton: React.FC<FluidButtonProps> = ({ : `/consumption/${FluidType[fluidType].toLowerCase()}` ) }, [history, fluidType, client]) + + useEffect(() => { + //Show errors only on konnected konnectors that are in error, outdated, with no data (specific case), and not in multifluid + if ( + (fluidType !== FluidType.MULTIFLUID && isConnected() && isErrored()) || + (fluidType !== FluidType.MULTIFLUID && isConnected() && isOutdated()) || + (isConnected() && + fluidStatus[fluidType] && + !fluidStatus[fluidType].lastDataDate) + ) { + setShowError(true) + } + }, [fluidStatus, fluidType, isConnected, isErrored, isOutdated]) return ( <div className={`fluid-title ${FluidType[ @@ -68,7 +92,7 @@ const FluidButton: React.FC<FluidButtonProps> = ({ icon={iconType} size={fluidType === FluidType.MULTIFLUID ? 36 : 32} /> - {isErrored() && ( + {showError && ( <StyledIcon icon={ErrorNotif} size={22} className="notif-error" /> )} <div diff --git a/src/components/Home/OldFluidDataModal.spec.tsx b/src/components/Home/OldFluidDataModal.spec.tsx deleted file mode 100644 index 19af0e9fc..000000000 --- a/src/components/Home/OldFluidDataModal.spec.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import { shallow } from 'enzyme' -import OldFluidDataModal from 'components/Home/OldFluidDataModal' -import { FluidType } from 'enum/fluid.enum' - -jest.mock('cozy-ui/transpiled/react/I18n', () => { - return { - useI18n: jest.fn(() => { - return { - t: (str: string) => str, - } - }), - } -}) -const handleCloseClick = jest.fn() - -describe('OldFluidDataModal component', () => { - it('should be rendered correctly', () => { - const component = shallow( - <OldFluidDataModal - open={true} - handleCloseClick={handleCloseClick} - fluidStatus={[]} - fluidOldData={[FluidType.ELECTRICITY, FluidType.GAS]} - /> - ).getElement() - expect(component).toMatchSnapshot() - }) - it('should render errored konnectors', () => { - //TODO Change useState konnectorError in order to test the other part of component - const wrapper = shallow( - <OldFluidDataModal - open={true} - handleCloseClick={handleCloseClick} - fluidStatus={[]} - fluidOldData={[FluidType.ELECTRICITY, FluidType.GAS]} - /> - ) - - setTimeout(() => { - expect(wrapper.find('.buttons').find('.btn-highlight').length).toEqual(1) - }, 300) - }) - it('should render fluid provider error', () => { - const wrapper = shallow( - <OldFluidDataModal - open={true} - handleCloseClick={handleCloseClick} - fluidStatus={[]} - fluidOldData={[FluidType.ELECTRICITY, FluidType.GAS]} - /> - ) - expect(wrapper.exists('.providerProblem')).toEqual(true) - }) -}) diff --git a/src/components/Home/OldFluidDataModal.tsx b/src/components/Home/OldFluidDataModal.tsx deleted file mode 100644 index be96181ca..000000000 --- a/src/components/Home/OldFluidDataModal.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react' -import './oldFluidDataModal.scss' -import { useI18n } from 'cozy-ui/transpiled/react/I18n' -import { useHistory } from 'react-router-dom' - -import { FluidStatus } from 'models' -import { FluidState, FluidType } from 'enum/fluid.enum' -import WarnCross from 'assets/icons/ico/warn-cross.svg' -import Icon from 'cozy-ui/transpiled/react/Icon' -import Dialog from '@material-ui/core/Dialog' -import Button from '@material-ui/core/Button' - -interface OldFluidDataModalProps { - open: boolean - fluidStatus: FluidStatus[] - fluidOldData: FluidType[] - handleCloseClick: () => void -} - -const OldFluidDataModal: React.FC<OldFluidDataModalProps> = ({ - open, - fluidStatus, - fluidOldData, - handleCloseClick, -}: OldFluidDataModalProps) => { - const { t } = useI18n() - const history = useHistory() - const [konnectorError, setkonnectorError] = useState<boolean>(false) - const [erroredKonnectors] = useState<FluidType[]>([]) - - const checkFluidDataDate = useCallback(() => { - fluidStatus && - fluidStatus.length > 0 && - fluidStatus.forEach(fluid => { - if (fluid.status === FluidState.ERROR) { - !erroredKonnectors.includes(fluid.fluidType) && - erroredKonnectors.push(fluid.fluidType) - } - }) - if (erroredKonnectors.length > 0) setkonnectorError(true) - }, [erroredKonnectors, fluidStatus]) - - const redirectToKonnectors = () => { - handleCloseClick() - history.push('/options') - } - - useEffect(() => { - checkFluidDataDate() - }, [checkFluidDataDate]) - - return ( - <Dialog - open={open} - onClose={handleCloseClick} - aria-labelledby={'accessibility-title'} - classes={{ - root: 'modal-root', - paper: 'modal-paper', - }} - > - <div id={'accessibility-title'}> - {t('old_fluid_data_modal.accessibility.window_title')} - </div> - <div className="od-content"> - <div className="od-warning"> - <Icon icon={WarnCross} size={40} /> - <p className="text-20-bold">{t('old_fluid_data_modal.errorTxt')}</p> - </div> - <p className="od-main text-16-bold"> - {' '} - {t('old_fluid_data_modal.oldData')} - </p> - {konnectorError ? ( - <div className="verifyState"> - <p className="text-16-normal">{t('old_fluid_data_modal.verify')}</p> - <ul className="od-konnectorsList"> - {erroredKonnectors.map((err, index) => { - return ( - <li key={index}> - {err === FluidType.ELECTRICITY && - t('FLUID.ELECTRICITY.LABEL')} - {err === FluidType.WATER && t('FLUID.WATER.LABEL')} - {err === FluidType.GAS && t('FLUID.GAS.LABEL')} - </li> - ) - })} - </ul> - <div className="buttons"> - <Button - aria-label={t( - 'old_fluid_data_modal.accessibility.button_later' - )} - onClick={handleCloseClick} - classes={{ - root: 'btn-secondary-positive', - label: 'text-16-normal', - }} - > - {t('old_fluid_data_modal.later')} - </Button> - <Button - aria-label={t( - 'old_fluid_data_modal.accessibility.button_goto_konnector' - )} - onClick={redirectToKonnectors} - classes={{ - root: 'btn-highlight', - label: 'text-16-bold', - }} - > - {t('old_fluid_data_modal.accessButton')} - </Button> - </div> - </div> - ) : ( - <div className="providerProblem"> - <p className="text-16-normal"> - {t('old_fluid_data_modal.problem')} - </p> - <ul className="od-konnectorsList"> - {fluidOldData.map((err, index) => { - return ( - <li key={index}> - {err === FluidType.ELECTRICITY && - `${t('FLUID.ELECTRICITY.provider')} ${t( - 'old_fluid_data_modal.problem_electricity' - )}`} - {err === FluidType.WATER && - `${t('FLUID.WATER.provider')} ${t( - 'old_fluid_data_modal.problem_water' - )}`} - {err === FluidType.GAS && - `${t('FLUID.GAS.provider')} ${t( - 'old_fluid_data_modal.problem_gas' - )}`} - </li> - ) - })} - </ul> - <p className="text-16-normal"> - {t('old_fluid_data_modal.contact')} - </p> - <Button - aria-label={t('old_fluid_data_modal.accessibility.button_ok')} - onClick={handleCloseClick} - classes={{ - root: 'btn-highlight', - label: 'text-16-bold', - }} - > - {t('old_fluid_data_modal.ok')} - </Button> - </div> - )} - </div> - </Dialog> - ) -} - -export default OldFluidDataModal diff --git a/src/components/Home/__snapshots__/OldFluidDataModal.spec.tsx.snap b/src/components/Home/__snapshots__/OldFluidDataModal.spec.tsx.snap deleted file mode 100644 index cbba9bf74..000000000 --- a/src/components/Home/__snapshots__/OldFluidDataModal.spec.tsx.snap +++ /dev/null @@ -1,81 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OldFluidDataModal component should be rendered correctly 1`] = ` -<WithStyles(ForwardRef(Dialog)) - aria-labelledby="accessibility-title" - classes={ - Object { - "paper": "modal-paper", - "root": "modal-root", - } - } - onClose={[MockFunction]} - open={true} -> - <div - id="accessibility-title" - > - old_fluid_data_modal.accessibility.window_title - </div> - <div - className="od-content" - > - <div - className="od-warning" - > - <Icon - icon="test-file-stub" - size={40} - spin={false} - /> - <p - className="text-20-bold" - > - old_fluid_data_modal.errorTxt - </p> - </div> - <p - className="od-main text-16-bold" - > - - old_fluid_data_modal.oldData - </p> - <div - className="providerProblem" - > - <p - className="text-16-normal" - > - old_fluid_data_modal.problem - </p> - <ul - className="od-konnectorsList" - > - <li> - FLUID.ELECTRICITY.provider old_fluid_data_modal.problem_electricity - </li> - <li> - FLUID.GAS.provider old_fluid_data_modal.problem_gas - </li> - </ul> - <p - className="text-16-normal" - > - old_fluid_data_modal.contact - </p> - <WithStyles(ForwardRef(Button)) - aria-label="old_fluid_data_modal.accessibility.button_ok" - classes={ - Object { - "label": "text-16-bold", - "root": "btn-highlight", - } - } - onClick={[MockFunction]} - > - old_fluid_data_modal.ok - </WithStyles(ForwardRef(Button))> - </div> - </div> -</WithStyles(ForwardRef(Dialog))> -`; diff --git a/src/components/Home/consumptionDetails.scss b/src/components/Home/consumptionDetails.scss index bfcec46d5..1c0333388 100644 --- a/src/components/Home/consumptionDetails.scss +++ b/src/components/Home/consumptionDetails.scss @@ -8,9 +8,9 @@ justify-content: center; width: 100%; box-sizing: border-box; - padding: 0.5rem 1rem; + padding: 0.5rem 1rem 1.5rem 1rem; @media #{$large-phone} { - margin-bottom: 0; + margin-bottom: 1rem; } .consumption-details-content { width: 45.75rem; diff --git a/src/components/Home/consumptionView.scss b/src/components/Home/consumptionView.scss index 7264e410d..7b60dd44a 100644 --- a/src/components/Home/consumptionView.scss +++ b/src/components/Home/consumptionView.scss @@ -19,6 +19,6 @@ margin-top: 1rem; @media #{$large-phone} { width: 100%; - padding: 1rem 1rem 4rem 1rem; + padding: 0rem 1rem 3rem 1rem; } } diff --git a/src/components/Home/oldFluidDataModal.scss b/src/components/Home/oldFluidDataModal.scss deleted file mode 100644 index 9704583f2..000000000 --- a/src/components/Home/oldFluidDataModal.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import 'src/styles/base/color'; - -.od-content { - padding: 1.5rem; - p { - color: $grey-bright; - } - .od-warning { - text-align: center; - color: $grey-bright; - - p { - margin: 1.125rem 0 2rem 0; - color: $red-primary; - } - } - .od-main { - color: $grey-bright; - } - .od-konnectorsList { - color: $gold-shadow; - } - .verifyState { - .buttons { - display: flex; - justify-content: space-between; - margin-top: 3rem; - button { - flex-basis: 48%; - padding: 0.25rem 0.5rem; - span:first-child { - line-height: 1; - } - &:nth-child(1) { - margin: 0 0.2rem 0 0; - } - &:nth-child(2) { - margin: 0 0 0 0.2rem; - } - } - } - } - .providerProblem { - text-align: center; - p, - ul { - text-align: left; - } - } -} - -#accessibility-title { - display: none; -} \ No newline at end of file diff --git a/src/components/Konnector/KonnectorViewerCard.tsx b/src/components/Konnector/KonnectorViewerCard.tsx index 04b429bb1..d1cda1f2d 100644 --- a/src/components/Konnector/KonnectorViewerCard.tsx +++ b/src/components/Konnector/KonnectorViewerCard.tsx @@ -17,7 +17,6 @@ import { FluidConnection, } from 'models' import { getAddPicto, getParamPicto } from 'utils/picto' -import { updateProfile } from 'store/profile/profile.actions' import { setChallengeConsumption } from 'store/challenge/challenge.actions' import { setFluidStatus, @@ -48,12 +47,14 @@ import { } from 'cozy-harvest-lib/dist/models/flowEvents' import { DateTime } from 'luxon' import { setSelectedDate } from 'store/chart/chart.actions' +import DateChartService from 'services/dateChart.service' interface KonnectorViewerCardProps { fluidStatus: FluidStatus isParam: boolean isDisconnected: boolean active: boolean + fluidType: FluidType setActive: React.Dispatch<React.SetStateAction<boolean>> } @@ -62,6 +63,7 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ isParam, isDisconnected, active, + fluidType, setActive, }: KonnectorViewerCardProps) => { const { t } = useI18n() @@ -83,6 +85,7 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ const [updatedFluidStatus, setUpdatedFluidStatus] = useState<FluidStatus[]>( [] ) + const [isOutdatedData, setisOutdatedData] = useState<number | null>(null) const { currentChallenge } = useSelector( (state: AppStore) => state.ecolyo.challenge ) @@ -96,10 +99,6 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ setActive(prev => !prev) } - const updateProfileHaveSeenOldFluidModal = useCallback(() => { - dispatch(updateProfile({ haveSeenOldFluidModal: false })) - }, [dispatch]) - const updateGlobalFluidStatus = useCallback(async (): Promise< FluidStatus[] > => { @@ -116,6 +115,7 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ if (lastDataDate > refDate) { dispatch(setSelectedDate(lastDataDate)) } + return _updatedFluidStatus }, [client, dispatch]) @@ -152,35 +152,37 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ }, [dispatch, setActive, updatedFluidStatus]) const handleAccountDeletion = useCallback(async () => { - updateProfileHaveSeenOldFluidModal() await refreshChallengeState() const _updatedFluidStatus = await updateGlobalFluidStatus() if (_updatedFluidStatus.length > 0) { dispatch(setFluidStatus(_updatedFluidStatus)) } setActive(false) - }, [ - updateProfileHaveSeenOldFluidModal, - refreshChallengeState, - updateGlobalFluidStatus, - setActive, - dispatch, - ]) + }, [refreshChallengeState, updateGlobalFluidStatus, setActive, dispatch]) const getConnectionCard = useCallback(() => { - if (fluidState === FluidState.KONNECTOR_NOT_FOUND) { + if (fluidState === FluidState.KONNECTOR_NOT_FOUND && !isUpdating) { return <ConnectionNotFound konnectorSlug={fluidSlug} /> } else if (account && fluidState !== FluidState.ERROR_LOGIN_FAILED) { return ( <ConnectionResult fluidStatus={fluidStatus} handleAccountDeletion={handleAccountDeletion} + fluidType={fluidType} /> ) } else { return <ConnectionForm fluidStatus={fluidStatus} /> } - }, [fluidSlug, fluidState, account, fluidStatus, handleAccountDeletion]) + }, [ + fluidState, + isUpdating, + account, + fluidSlug, + fluidStatus, + handleAccountDeletion, + fluidType, + ]) const callbackResponse = useCallback( async (_state: string) => { @@ -189,7 +191,6 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ shouldLaunchKonnector: false, } dispatch(updatedFluidConnection(fluidStatus.fluidType, updatedConnection)) - updateProfileHaveSeenOldFluidModal() await refreshChallengeState() await updateGlobalFluidStatus() setKonnectorState(_state) @@ -198,7 +199,6 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ dispatch, fluidStatus.connection, fluidStatus.fluidType, - updateProfileHaveSeenOldFluidModal, refreshChallengeState, updateGlobalFluidStatus, ] @@ -232,6 +232,11 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ } } getData() + const dateChartService = new DateChartService() + + setisOutdatedData( + dateChartService.isDataOutdated(fluidStatus.lastDataDate, fluidType) + ) return () => { subscribed = false } @@ -244,8 +249,9 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ fluidStatus.fluidType, callbackResponse, setActive, + fluidStatus.lastDataDate, + fluidType, ]) - return ( <> {!isDisconnected ? ( @@ -255,7 +261,8 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ classes={{ root: `expansion-panel-root ${ fluidStatus.status === FluidState.ERROR || - fluidStatus.status === FluidState.ERROR_LOGIN_FAILED + fluidStatus.status === FluidState.ERROR_LOGIN_FAILED || + isOutdatedData ? 'red-border' : '' }`, @@ -297,13 +304,20 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ })} > {fluidStatus.connection.account && - fluidStatus.status !== FluidState.ERROR_LOGIN_FAILED - ? t('FLUID.' + FluidType[fluidStatus.fluidType] + '.LABEL') - : t( - `konnector_options.label_connect_to_${FluidType[ - fluidStatus.fluidType - ].toLowerCase()}` - )} + fluidStatus.status !== FluidState.ERROR_LOGIN_FAILED && + !isOutdatedData ? ( + t('FLUID.' + FluidType[fluidStatus.fluidType] + '.LABEL') + ) : fluidStatus.status !== FluidState.ERROR_LOGIN_FAILED ? ( + <span className="text-16-bold outdated"> + Données manquantes depuis {isOutdatedData} jours + </span> + ) : ( + t( + `konnector_options.label_connect_to_${FluidType[ + fluidStatus.fluidType + ].toLowerCase()}` + ) + )} </div> </ExpansionPanelSummary> <ExpansionPanelDetails diff --git a/src/components/Konnector/konnectorViewerCard.scss b/src/components/Konnector/konnectorViewerCard.scss index 03a2d2a76..3eaa94ce1 100644 --- a/src/components/Konnector/konnectorViewerCard.scss +++ b/src/components/Konnector/konnectorViewerCard.scss @@ -1,7 +1,7 @@ @import 'src/styles/base/color'; @import 'src/styles/base/breakpoint'; -.konnector-icon{ +.konnector-icon { margin-right: 1rem; position: relative; @media #{$large-phone} { @@ -14,7 +14,7 @@ } } -.konnector-title{ +.konnector-title { color: $grey-bright; &.electricity-connected { @extend .konnector-title; @@ -28,4 +28,7 @@ @extend .konnector-title; color: $water-color; } -} \ No newline at end of file + .outdated { + color: $grey-bright; + } +} diff --git a/src/db/profileData.json b/src/db/profileData.json index b9576397f..1bd22593e 100644 --- a/src/db/profileData.json +++ b/src/db/profileData.json @@ -7,7 +7,6 @@ "quizHash": "", "isFirstConnection": true, "lastConnectionDate": "0000-01-01T00:00:00.000Z", - "haveSeenOldFluidModal": false, "haveSeenLastAnalysis": true, "sendAnalysisNotification": true, "monthlyAnalysisDate": "0000-01-01T00:00:00.000Z", diff --git a/src/locales/fr.json b/src/locales/fr.json index dea81baf7..35d2e1000 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -237,11 +237,19 @@ "mini_cards_label": "AUTRES ENERGIES" }, "consumption_visualizer": { - "no_data": "Données manquantes", + "missing_data": "Données manquantes", + "no_data": "Pas de données", + "why_no_data": "Pourquoi n'ai-je pas de données ?", "last_data": "Dernières données", "last_valid_data": "Dernières données valides", "data_to_come": "à venir", + "aie": "Aïe !", "estimated": "estimés", + "dataModal": { + "list_title": "2 raisons possibles :", + "item1": "- le lien entre Ecolyo et le fournisseur de données est rompu : une mise à jour de ce lien (en bas de la page) peut parfois résoudre ce problème.", + "item2": "- la mise à jour a été effectuée mais les données n'apparaîssent toujours pas : le soucis vient du fournisseur de données (Enedis pour l'électricité, GRDF pour le gaz, Eau du Grand Lyon pour l'eau). Cela peut-être dû à un problème technique et le mieux est de contacter directement votre fournisseur pour tenter de régler le problème avec eux." + }, "modal": { "window_title": "info estimation des prix", "title": "Comment sont estimés les prix ?", @@ -497,9 +505,11 @@ } }, "konnector_form": { - "label_updated_at": "dernière mise-à-jour le", + "label_updated_at": "Dernière mise-à-jour le", "button_update": "Mettre à jour", "button_delete": "Supprimer", + "issue": "Le problème semble venir de ", + "resolve": "Une mise à jour peut résoudre ce problème.", "not_installed": "Le connecteur n'est pas installé. Veuillez l'installer en cliquant sur le bouton ci-dessous.", "button_install": "Installer", "error_account_creation": "Une erreur est survenue, veuillez essayer de nouveau.", diff --git a/src/models/profile.model.ts b/src/models/profile.model.ts index 2ace99cb9..4b7960cdb 100644 --- a/src/models/profile.model.ts +++ b/src/models/profile.model.ts @@ -14,7 +14,6 @@ export interface ProfileEntity { isFirstConnection: boolean lastConnectionDate: string haveSeenLastAnalysis: boolean - haveSeenOldFluidModal: string | boolean sendAnalysisNotification: boolean monthlyAnalysisDate: string sendConsumptionAlert: boolean @@ -27,11 +26,7 @@ export interface ProfileEntity { } export interface Profile - extends Omit< - ProfileEntity, - 'haveSeenOldFluidModal' | 'lastConnectionDate' | 'monthlyAnalysisDate' - > { + extends Omit<ProfileEntity, 'lastConnectionDate' | 'monthlyAnalysisDate'> { lastConnectionDate: DateTime - haveSeenOldFluidModal: DateTime | boolean monthlyAnalysisDate: DateTime } diff --git a/src/services/dateChart.service.spec.ts b/src/services/dateChart.service.spec.ts index f0383d29b..606c816f6 100644 --- a/src/services/dateChart.service.spec.ts +++ b/src/services/dateChart.service.spec.ts @@ -1312,9 +1312,9 @@ describe('dateChart service', () => { expect(result).toBe(true) }) - it('should return false for electricity fluidType and dataLoad with date >= today-1', () => { + it('should return false for electricity fluidType and dataLoad with date > J+3', () => { const dataLoad: Dataload = { - date: today.plus({ day: -1 }), + date: today.plus({ day: -4 }), value: 0, valueDetail: null, } @@ -1335,9 +1335,9 @@ describe('dateChart service', () => { expect(result).toBe(true) }) - it('should return false for gaz fluidType and dataLoad with date >= today-3', () => { + it('should return false for gaz fluidType and dataLoad with date > J+5', () => { const dataLoad: Dataload = { - date: today.plus({ day: -3 }), + date: today.plus({ day: -6 }), value: 0, valueDetail: null, } @@ -1355,9 +1355,9 @@ describe('dateChart service', () => { expect(result).toBe(true) }) - it('should return false for water fluidType and dataLoad with date >= today-3', () => { + it('should return false for water fluidType and dataLoad with date > J+5', () => { const dataLoad: Dataload = { - date: today.plus({ day: -3 }), + date: today.plus({ day: -6 }), value: 0, valueDetail: null, } diff --git a/src/services/dateChart.service.ts b/src/services/dateChart.service.ts index 8a771d6e3..b29c55e39 100644 --- a/src/services/dateChart.service.ts +++ b/src/services/dateChart.service.ts @@ -2,7 +2,7 @@ import { DateTime, Interval } from 'luxon' import { FluidType } from 'enum/fluid.enum' import { TimeStep } from 'enum/timeStep.enum' -import { Dataload, TimePeriod } from 'models' +import { Datachart, Dataload, FluidConfig, TimePeriod } from 'models' import ConfigService from './fluidConfig.service' export default class DateChartService { @@ -303,30 +303,89 @@ export default class DateChartService { const inter = dataload && Interval.fromDateTimes( - dataload.date, - DateTime.local().setZone('utc', { - keepLocalTime: true, - }) + dataload.date.startOf('day'), + DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day') ).count('days') if ( fluidType === FluidType.ELECTRICITY && - inter < fluidConfig[0].dataDelayOffset + 1 + inter <= fluidConfig[0].dataDelayOffset + 3 ) { return true } if ( fluidType === FluidType.WATER && - inter < fluidConfig[1].dataDelayOffset + 1 + inter <= fluidConfig[1].dataDelayOffset + 3 ) { return true } if ( fluidType === FluidType.GAS && - inter < fluidConfig[2].dataDelayOffset + 1 + inter <= fluidConfig[2].dataDelayOffset + 3 ) { return true } else { return false } } + + /** + * Checks if there is data after a lack of data so we know if it is a hole or not + * @param {Datachart} currentDatachart + * @returns + */ + public isDataHole( + currentDatachart: Datachart, + fluidType?: FluidType + ): boolean { + let isDataHole = false + let isEmpty = false + if (fluidType || fluidType === 0) { + currentDatachart.actualData.forEach((data: Dataload) => { + if (data.valueDetail && data.valueDetail[fluidType] === -1) { + isEmpty = true + } + if (data.valueDetail && data.valueDetail[fluidType] > -1 && isEmpty) + isDataHole = true + }) + } else { + currentDatachart.actualData.forEach((data: Dataload) => { + if (data.value === -1) { + isEmpty = true + } + if (data.value > -1 && isEmpty) isDataHole = true + }) + } + if (isDataHole) { + return true + } else { + return false + } + } + + /** + * Checks if the last data date is outdated and returns the number of missing days + * @param {DateTime | null} lastDataDate + * @param {FluidType} fluidType + * @returns {number| null} - The number of missing days + */ + public isDataOutdated( + date: DateTime | null, + fluidType: FluidType + ): number | null { + if (date && fluidType !== FluidType.MULTIFLUID) { + const fluidConfig: Array<FluidConfig> = new ConfigService().getFluidConfig() + const today = DateTime.local().setZone('utc', { + keepLocalTime: true, + }) + const inter = Interval.fromDateTimes(date, today).count('days') + const limitDate = fluidConfig[fluidType].dataDelayOffset + 1 + if (inter > limitDate) { + return inter - limitDate + } else return null + } else return null + } } diff --git a/src/services/profile.service.spec.ts b/src/services/profile.service.spec.ts index 09cf8e6c7..47e6af389 100644 --- a/src/services/profile.service.spec.ts +++ b/src/services/profile.service.spec.ts @@ -21,10 +21,9 @@ describe('UserProfile service', () => { expect(result).toEqual(profileData) }) - it('shoud return the user profile from string haveSeenOldFluidModal & monthlyAnalysisDate', async () => { + it('shoud return the user profile from string monthlyAnalysisDate', async () => { const userProfile = { ...profileData, - haveSeenOldFluidModal: '2020-11-09T11:27:30.073Z', monthlyAnalysisDate: '2020-11-09T11:27:30.073Z', } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -37,9 +36,6 @@ describe('UserProfile service', () => { mockClient.query.mockResolvedValueOnce(mockQueryResult) const resultUserProfile = { ...profileData, - haveSeenOldFluidModal: DateTime.fromISO('2020-11-09T11:27:30.073Z', { - zone: 'utc', - }), monthlyAnalysisDate: DateTime.fromISO('2020-11-09T11:27:30.073Z', { zone: 'utc', }), @@ -65,7 +61,6 @@ describe('UserProfile service', () => { it('shoud return an updated user profile', async () => { const userProfile = { ...profileData, - haveSeenOldFluidModal: '2020-11-09T11:27:30.073Z', monthlyAnalysisDate: '2020-11-03T00:00:00.000Z', } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -90,9 +85,6 @@ describe('UserProfile service', () => { mockClient.save.mockResolvedValueOnce(mockUpdatedQueryResult) const resultUserProfile = { ...profileData, - haveSeenOldFluidModal: DateTime.fromISO('2020-11-09T11:27:30.073Z', { - zone: 'utc', - }), monthlyAnalysisDate: DateTime.fromISO('2020-11-03T00:00:00.000Z', { zone: 'utc', }), diff --git a/src/services/profile.service.ts b/src/services/profile.service.ts index 5a8836af3..73e5d8f44 100644 --- a/src/services/profile.service.ts +++ b/src/services/profile.service.ts @@ -1,23 +1,7 @@ -import { - Client, - MongoSelector, - Q, - QueryDefinition, - QueryResult, -} from 'cozy-client' -import { Profile, ProfileEntity, ProfileType } from 'models' -import { PROFILETYPE_DOCTYPE, PROFILE_DOCTYPE } from 'doctypes' +import { Client, Q, QueryDefinition, QueryResult } from 'cozy-client' +import { Profile, ProfileEntity } from 'models' +import { PROFILE_DOCTYPE } from 'doctypes' import { DateTime } from 'luxon' -import { - ConstructionYear, - Floor, - HotWaterEquipment, - HousingType, - IndividualInsulationWork, - IndividualOrCollective, - OutsideFacingWalls, - ThreeChoicesAnswer, -} from 'enum/profileType.enum' export default class ProfileService { private readonly _client: Client @@ -33,12 +17,6 @@ export default class ProfileService { private parseProfileEntityToProfile(profileEntity: ProfileEntity): Profile { const profile: Profile = { ...profileEntity, - haveSeenOldFluidModal: - typeof profileEntity.haveSeenOldFluidModal === 'string' - ? DateTime.fromISO(profileEntity.haveSeenOldFluidModal, { - zone: 'utc', - }) - : profileEntity.haveSeenOldFluidModal, monthlyAnalysisDate: typeof profileEntity.monthlyAnalysisDate === 'string' ? DateTime.fromISO(profileEntity.monthlyAnalysisDate, { diff --git a/src/store/profile/profile.reducer.ts b/src/store/profile/profile.reducer.ts index 6fbcae1dc..93af8b3c4 100644 --- a/src/store/profile/profile.reducer.ts +++ b/src/store/profile/profile.reducer.ts @@ -15,7 +15,6 @@ const initialState: Profile = { explorationHash: '', isFirstConnection: false, lastConnectionDate: DateTime.fromISO('0000-01-01T00:00:00.000Z'), - haveSeenOldFluidModal: true, haveSeenLastAnalysis: true, sendAnalysisNotification: true, sendConsumptionAlert: false, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 45f940a60..cfd513296 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -42,10 +42,7 @@ export function isKonnectorActive( return true } } - if ( - fluidStatus[fluidType].status === FluidState.NOT_CONNECTED || - fluidStatus[fluidType].status === FluidState.ERROR_LOGIN_FAILED - ) { + if (fluidStatus[fluidType].status === FluidState.NOT_CONNECTED) { return false } else return true } diff --git a/tests/__mocks__/profile.mock.ts b/tests/__mocks__/profile.mock.ts index 46410205e..2faa1510b 100644 --- a/tests/__mocks__/profile.mock.ts +++ b/tests/__mocks__/profile.mock.ts @@ -17,7 +17,6 @@ export const profileData: Profile = { lastConnectionDate: DateTime.fromISO('2020-11-03T00:00:00.000Z', { zone: 'utc', }), - haveSeenOldFluidModal: false, haveSeenLastAnalysis: true, monthlyAnalysisDate: DateTime.fromISO('2020-11-03T00:00:00.000Z', { zone: 'utc', diff --git a/tests/__mocks__/store.ts b/tests/__mocks__/store.ts index 1fe5285dc..3c20e9e48 100644 --- a/tests/__mocks__/store.ts +++ b/tests/__mocks__/store.ts @@ -106,7 +106,6 @@ export const mockInitialProfileState: Profile = { explorationHash: '', isFirstConnection: false, lastConnectionDate: DateTime.fromISO('0000-01-01T00:00:00.000Z'), - haveSeenOldFluidModal: true, haveSeenLastAnalysis: true, sendConsumptionAlert: false, waterDailyConsumptionLimit: 0, -- GitLab