diff --git a/src/assets/icons/ico/questionMark.svg b/src/assets/icons/ico/questionMark.svg new file mode 100644 index 0000000000000000000000000000000000000000..b633290c9775786a543936c2b65803b7203a755e --- /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 518f34863d5e376c1a181e7a822b32679da95427..3e93db6b5f100870fe582e041dfddf6d959213be 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 5e81b045002b95ad56b5858202e86b63349a1aef..15b4af1c64dae9f6eb1ecabdfde58f12e6ad147c 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 6c37d89c67251f6bb7fb995bb6504b27c7dda219..cf5f6477a5d5914c15f78385e3041f8cd2fab937 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 0000000000000000000000000000000000000000..f8823fc779403c72bcfce5b07504cfc1bbbb38c9 --- /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 0000000000000000000000000000000000000000..69fb32a16d921bf052335710e3c311131e1f1dde --- /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 67396474a5194554177995bc14f113335b477a64..6412b5a3a7ca6456a8aeca207fba2760679b16fb 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 0000000000000000000000000000000000000000..3a2cf00dafef1534959dc6f6959c8046e59a5d29 --- /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 0000000000000000000000000000000000000000..8ae100b80d16c39f844c96405cab0b03b182d661 --- /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 0000000000000000000000000000000000000000..2f56ebdb10d2eaf4308a240f3baa12bd840f57c6 --- /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 e2f7780658ba528f8ec6761c9e248cc6c66fb059..7f4c4c3c47d81d04204b50f5716b214c2cd3df56 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 0000000000000000000000000000000000000000..b299ed7ac8c16f6e5166ea0356b2a95cac759350 --- /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 6d2a32803b236744154d7fe2efe78c50f53d5fd5..d395f63d9f2a7ab02554509372614336dead7c7e 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 0000000000000000000000000000000000000000..d233665e12ecacb0436078ecd81839f41acd4d83 --- /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 0000000000000000000000000000000000000000..f6c64f7a1ce587b23504a0aa0036cb25b87f5f40 --- /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 0000000000000000000000000000000000000000..9f5722378e8098a9a2321df5c7889b97c9915583 --- /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 0000000000000000000000000000000000000000..372c0275b86ca025a7d34130d5e958f91305de12 --- /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 0000000000000000000000000000000000000000..a95a17a508f07a2b3deb2ec4954baee60bae12e4 --- /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 0000000000000000000000000000000000000000..c10fc2c6145b8fb653eec9d764fc072a23347229 --- /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 0000000000000000000000000000000000000000..f7551fdfb8bdd9385a89819055da7cad08d48173 --- /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 4b5fc3f8a379e91bd7c343fd7daddad238cf4b88..d9b772b795430ac83179ad35cbbd61ec39bd8a6a 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 c5659df8e8f9870bfa62b5b1a2a108c3db436f66..d7fe9d5b86f206e1a96cf104047ae6cd4320de80 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 f53c69afaf39ade6892966d2a1947198d0654174..9482a48fbe0c8ce703a8b5c13fccc6104bee6aca 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 5a85afc6488c765c77a13c652af4c52569a51884..381b55bba06ac4187f387a8fcb115aee7a935f73 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 0000000000000000000000000000000000000000..f2e825050705a7808030a5dee1bac7232012e18f --- /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 3375fad3a67112755f17baf17126f2f84cceecfb..d25aa6efad6ed3793f4b5daccd0a7018850442c6 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 b3be150767908664337895347cdf5e264cb46d41..c459e9c770eac57c3004a309f1742baa663181c2 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 777b8bd51c35d491ba362fa0e609754c5989eaa9..246291866f230f0d6675eb1dfe3a4ed91aa2c397 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 e3dfd501719afc8f8af171f048f42e3074106c03..cd629bab5d4e630bc397840ea8f3e94852a9db68 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 c50430cc9d8a1053d6563ecf06e023d0780acece..8051b2ceaa8349639139fdcd8504e1ffe5e01337 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 a3b380e49bb14466ecb7623b2047abf19f0ebf11..7be422f8be2fd31267a14492ae0da2ac6bc81d67 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 19af0e9fc5f9f2914f53a1f2d0b8b30a25cbf421..0000000000000000000000000000000000000000 --- 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 be96181ca404369e9df34c7f176a66e7259f1bd0..0000000000000000000000000000000000000000 --- 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 cbba9bf741f3b90fe4194e94c22097a2987003ae..0000000000000000000000000000000000000000 --- 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 bfcec46d586c97406bc344a6c0966a6f161408d8..1c03333888e4bb6cc0b1a1bb2829798e5d27310b 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 7264e410d0e4a26afcef23a3df43cf968d7ad060..7b60dd44a4e607cb79d53ff3e821f4bbd36b2d9c 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 9704583f22bf064cdabe9399e69855a6c30dc880..0000000000000000000000000000000000000000 --- 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 04b429bb1e3892e2b21be206efbec88e467455e4..d1cda1f2d68635b0fdc8f431ae81f734488a7be1 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 03a2d2a76270da208397ede4f6ce6eccdb77f3f9..3eaa94ce1819ed36451695e766475733da0df6fb 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 b9576397f31cc2c0c4af7361f7138437f00761ce..1bd22593ed9908c799b69c187019cf8dbcc8c5ea 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 dea81baf71e9e9c4de192b8a9921ec5553556a6a..35d2e1000580447832ebf95d2d5b60982eb72dd8 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 2ace99cb90768cc9c9099b922c853e1e7bc84cc4..4b7960cdbf49b0d64194d0b516c9ebfad6825ff9 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 f0383d29bb20fd25f0973249e0b7eec343e986d0..606c816f635dcb58bc4e5caeb1c0e834397e49d9 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 8a771d6e3b5b2675074a48fffe6a1ffcf0c0b717..b29c55e398cfb83c1b849099f7e06aa1e8fa5e73 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 09cf8e6c7dfa68b5dea22a298a9a932b31ae5bd1..47e6af389fea3612953a00cdc0ad0c886e184136 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 5a8836af36c5c19f7a4c665e83753a28d146e24c..73e5d8f44bd70f9d4ad39bbb81bbe154d9a779da 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 6fbcae1dca6251547cc79eba34073b452b06c7b0..93af8b3c429357eda8d9e4cd64a1b483f0a74a5b 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 45f940a609cbcb16fc4b1976599c9bb6e642854e..cfd513296e0f8ca840007204bd6101dc50cbe36f 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 46410205ef1cc75449b5012fa8b46e4165050028..2faa1510bdad15630f36dbac41b8cc254b8998a5 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 1fe5285dc9eb483d33d581a0076ff454bd861395..3c20e9e48864cde305ae0dfcec138aec9e406cfc 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,